
<!DOCTYPE html>
<html i18n-values="
    dir:textdirection;
    hasattribution:hasattribution;
    customlogo:customlogo;
    themegravity:themegravity;"
  class="starting-up">
<head>
<meta charset="utf-8">
<title i18n-content="title"></title>
<!-- Don't scale the viewport in either portrait or landscape mode.
     Note that this means apps will be reflowed when rotated (like iPad).
     If we wanted to maintain position we could remove 'maximum-scale' so
     that we'd zoom out in portrait mode, but then there would be a bunch
     of unusable space at the bottom.
-->
<meta name="viewport"
      content="user-scalable=no, width=device-width, maximum-scale=1.0">

<!-- It's important that this be the first script loaded. -->
<script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileOverview
 * Logging info for benchmarking purposes. Should be the first js file included.
 */

/* Stack of events that has been logged. */
var eventLog = [];

/**
 * Logs an event.
 * @param {String} name The name of the event (can be any string).
 * @param {boolean} shouldLogTime If true, the event is used for benchmarking
 *     and the time is logged. Otherwise, just push the event on the event
 *     stack.
 */
function logEvent(name, shouldLogTime) {
  if (shouldLogTime)
    chrome.send('logEventTime', [name]);
  eventLog.push([name, Date.now()]);
}

logEvent('Tab.NewTabScriptStart', true);
window.addEventListener('load', function(e) {
  logEvent('Tab.NewTabOnload', true);
});
document.addEventListener('DOMContentLoaded', function(e) {
  logEvent('Tab.NewTabDOMContentLoaded', true);
});
</script>



<style>/* Copyright (c) 2011 The Chromium Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

.bubble {
  /* These margins correlate to the difference between the tip of the arrow
   * and the bottom left of the bubble. */
  margin-left: -30px;
  margin-top: -8px;
  position: absolute;
  /* Height is dynamic, width fixed. */
  width: 300px;
  z-index: 9999;
}

.bubble-contents {
  color: black;
  left: 1px;
  line-height: 150%;
  padding: 8px 11px 12px;
  position: relative;
  right: 1px;
  top: 1px;
  width: 298px;
  z-index: 3;
}

.bubble-close {
  background-image: no-repeat 50% 50%;  
  height: 16px;
  position: absolute;
  right: 6px;
  top: 6px;
  width: 16px;
  z-index: 4;
}

.bubble-close {
  background-image: url('chrome://theme/IDR_CLOSE_BAR');
}

.bubble-close:hover {
  background-image: url('chrome://theme/IDR_CLOSE_BAR_H');
}

.bubble-close:active {
  background-image: url('chrome://theme/IDR_CLOSE_BAR_P');
}

.bubble-shadow {
  bottom: -2px;
  left: 0;
  position: absolute;
  right: 0;
  top: 0;
  z-index: 1;
}

.bubble-arrow {
  /* The tip of the arrow.  */
  border-bottom-right-radius: 1px;
  /* No border on the right or top (inner sides of the rotated square) because
   * it would overlap/darken the content shadow. */
  border-right: none;
  border-top: none;
  bottom: -9px;
  height: 15px;
  left: 22px;
  position: absolute;
  right: 22px;
  width: 15px;
  z-index: 2;
  -webkit-transform: rotate(45deg);
}

.bubble-contents,
.bubble-arrow {
  background: white;
}

.bubble-shadow,
.bubble-arrow {
  border: 1px solid rgba(0, 0, 0, 0.3);
  -webkit-box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.15);
}

.bubble-shadow,
.bubble-contents {
  border-radius: 6px;
  box-sizing: border-box;
}
</style>
<style>menu {
  -webkit-box-shadow: 0px 2px 4px rgba(0, 0, 0, .50);
  background: white;
  color: black;
  cursor: default;
  display: none;
  left: 0;
  margin: 0;
  outline: 1px solid rgba(0, 0, 0, 0.2);
  padding: 8px 0;
  position: fixed;
  white-space: nowrap;
  z-index: 3;
}

menu > * {
  box-sizing: border-box;
  display: block;
  margin: 0;
  width: 100%;
  text-align: start;
}

menu > :not(hr) {
  -webkit-appearance: none;
  background: transparent;
  border: 0;
  font: inherit;
  line-height: 18px;
  overflow: hidden;
  padding: 0 19px;
  text-overflow: ellipsis;
}

menu > hr {
  background: -webkit-linear-gradient(left,
                                      rgba(0, 0, 0, .10),
                                      rgba(0, 0, 0, .02) 96%);
  border: 0;
  height: 1px;
  margin: 8px 0;
}

menu > [disabled] {
  color: rgba(0, 0, 0, .3);
}

/* Text alpha doesn't work on Linux/GTK and ChromeOs/views.
 * TODO(estade): http://crbug.com/88688. */
html[toolkit=gtk] menu > [disabled],
html[os=chromeos] menu > [disabled] {
  color: #969696;
}

menu > [hidden] {
  display: none;
}

menu > :not(hr)[selected] {
  background-color: #dce5fa;
}

menu > :not(hr)[selected]:active {
  background-color: #426dc9;
  color: #fff;
}

menu > [checked]:before {
  content: url("");
  display: inline-block;
  height: 9px;
  margin: 0 5px;
  vertical-align: 50%;
  width: 9px;
}

menu > [checked] {
  -webkit-padding-start: 0;
}

menu > [selected][checked]:active:before {
  content: url("");
}
</style>
<style>/* Copyright (c) 2011 The Chromium Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

.app {
  position: absolute;
  text-align: center;
  -webkit-transition: -webkit-transform 0.1s;
}

.app:active:not(.suppress-active),
.dragging .app,
.drag-representation .app {
  /* Don't animate the initial scaling.  */
  -webkit-transition-duration: 0;
  /* We need to specify both scale and rotate because the transform in
     .tile.drag-representation.deleting specifies scale and rotate. */
  -webkit-transform: scale(1.1) rotate(0);
}

/* Active gets applied right before .suppress-active, so to avoid flicker
 * we need to make the scale go back to normal without an animation. */
.app.suppress-active {
  -webkit-transition-duration: 0;
}

.app-contents > span {
  cursor: pointer;
  display: block;
  overflow: hidden;
  text-decoration: none;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.app-img-container {
  cursor: pointer;
  margin-left: auto;
  margin-right: auto;
  /* -webkit-mask-image set by JavaScript to the image source */
  -webkit-mask-size: 100% 100%;
}

.app-img-container > * {
  height: 100%;
  width: 100%;
}

.app-icon-div {
  background-color: white;
  border: 1px solid #d5d5d5;
  border-radius: 5px;
  cursor: pointer;
  display: -webkit-box;
  margin-left: auto;
  margin-right: auto;
  position: relative;
  vertical-align: middle;
  -webkit-box-align: center;
  -webkit-box-pack: center;
}

.app-icon-div .color-stripe {
  border-bottom-left-radius: 5px 5px;
  border-bottom-right-radius: 5px 5px;
  bottom: 0px;
  height: 3px;
  opacity: 1.0;
  position: absolute;
  width: 100%;
  z-index: 100;
}

.app-context-menu > button:first-child {
  font-weight: bold;
}

.app-context-menu {
  z-index: 1000;
}

/* Notifications */

.app-notification {
  color: #999999;  
  display: block;
  font-size: 11px;
  white-space: nowrap;
  -webkit-transition: color .15s linear;
}

.app-notification:hover {
  text-decoration: underline;
}

/* Promo */
/* Show the promo if the webstore has a promo and is the only app on the page.
 */
.tile:only-of-type > .has-promo > .app-contents > span {
  display: none;
}

.tile:only-of-type > .has-promo .app-img-container > .apps-promo-logo {
  display: block;
}

.apps-promo-logo {
  display: none;
}

.tile:only-of-type > .has-promo .app-img-container > img:first-child {
  display: none;
}

.app-img-container > img:first-child {
  display: block;
}

/* TODO(estade): animation? */
.tile:only-of-type > .has-promo > .apps-promo-extras {
  display: block;
}

.apps-promo-extras {
  display: none;
  position: absolute;
  text-align: left;
  top: 0;
}

.apps-promo-heading {
  font-weight: bold;
  margin-bottom: 5px;
  -webkit-margin-start: 3px;
}

.g-button-basic {
  border-width: 6px 10px 12px 6px;
  color: #fff !important;
  display: inline-block;
  font-size: 1.3em;
  font-weight: bold;
  padding: 2px 10px;
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  -webkit-border-image: url("") 6 10 12 6;
}
</style>
<style>/* Copyright (c) 2011 The Chromium Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

/* Bookmark Tiles ************************************************************/

.bookmark {
  display: -webkit-box;
  position: absolute;
  -webkit-box-orient: vertical;
}

.bookmark-fills-parent {
  bottom: 0;
  display: -webkit-box;
  left: 0;
  position: absolute;
  right: 0;
  top: 0;
}

.bookmark.removable .close-button {
  display: block;
  opacity: 0;
  position: absolute;
  right: 0;
  top: 0;
  z-index: 5;
  -webkit-transition: opacity 0.15s;
}

.bookmark .close-button {
  display: none;
}

html[dir=rtl] .bookmark .close-button {
  left: 0;
  right: auto;
}

.bookmark:hover .close-button {
  opacity: 1;
  -webkit-transition-delay: 0.5s;
}

.bookmark .favicon {
  background: no-repeat 5% 90%;
}

.bookmark .color-stripe {
  border-bottom-left-radius: 3px 3px;
  border-bottom-right-radius: 3px 3px;
  /* Matches height of title. */
  bottom: 23px;
  height: 3px;
  /* Matches padding-top of the title. */
  margin-bottom: 8px;
  position: absolute;
  width: 100%;
  z-index: 100;
}

.bookmark .title {
  cursor: pointer;
  display: block;
  height: 23px;
  overflow: hidden;
  padding-top: 8px;
  text-align: center;
  text-decoration: none;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bookmark .button {
  cursor: pointer;
  background-color: rgba(0, 0, 0, 0.15);
  border: 1px solid transparent;
  border-radius: 5px;
  bottom: 70px;
  position: relative;
  z-index: 5;
  -webkit-box-flex: 1;
  -webkit-transition: background-color .15s;  
}

.bookmark:hover .button {
  background-color: rgba(0, 0, 0, 0.3);
}

.bookmark .button-frame {
  background-color: #fff;
  border-radius: 5px;
}

.bookmark:hover .button-frame {
  background-color: #f7f7f7;
}

/* The bookmark gets a shadow when clicked, but not when the click is on the
 * close button. */
.bookmark:active .close-button:not(:active) + .button-frame {
  -webkit-box-shadow: inset 0 1px 10px rgba(0, 0, 0, 0.2);
}

/* Bookmark Titles ************************************************************/

#bookmarks-title-wrapper {
  background: #fff;
  background-position: left;
  background-repeat: no-repeat;
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
  color: #777;
  display: -webkit-box;
  font-size: 16px;
  height: 48px;
  line-height: 48px;
  overflow: hidden;
  white-space: nowrap;
}

#bookmarks-title-wrapper .title-crumb {
  cursor: pointer;
  margin: 0 25px;
}

#bookmarks-title-wrapper .title-crumb-active {
  color: #7f7f7f;
  cursor: default;
  font-weight: bold;
  margin-right: 0;
  padding-right: 10px;
}

.section-title {
  display: -webkit-box;
  left: 0;
  overflow: hidden;
  position: absolute;
  top: 0;
  -webkit-box-flex: 1;
  -webkit-box-orient: horizontal;
}

.section-title-mask {
  background: #fff;
  height: 100%;
  overflow: hidden;
  position: relative;
  -webkit-box-flex: 1;
}

.bookmark-separator {
  background: -webkit-canvas(bookmark-chevron) center center no-repeat;
  border: 0;
  height: 44px;
  margin: 2px 0;
  width: 10px;
}

#bookmarks-top-link-wrapper {
  left: 10px;
  position: absolute;
  right: 10px;
  text-align: end;
  top: 10px;
}

#bookmarks-bottom-link-wrapper {
  height: 40px;
  /* Matches .tile-page-content padding-right - 10px */
  margin: 0px -70px;
  text-align: end;
}
</style>
<style>/* Copyright (c) 2011 The Chromium Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

.most-visited {
  position: absolute;
}

.most-visited {
  display: -webkit-box;
  position: absolute;
  text-decoration: none;
  -webkit-box-orient: vertical;
}

.most-visited:focus {
  outline: none;
}

.fills-parent {
  bottom: 0;
  display: -webkit-box;
  left: 0;
  position: absolute;
  right: 0;
  top: 0;
}

/* filler mode: hide everything except the thumbnail --- leave a grey rectangle
 * in its place. */
.filler * {
  visibility: hidden;
}

.filler {
  pointer-events: none;
}

.most-visited .close-button {
  opacity: 0;
  position: absolute;
  right: 0;
  top: 0;
  z-index: 5;
  -webkit-transition: opacity 0.15s;
}

html[dir=rtl] .most-visited .close-button {
  left: 0;
  right: auto;
}

.most-visited:hover .close-button {
  opacity: 1;
  -webkit-transition-delay: 0.5s;
}

.close-button:hover {
  -webkit-transition-delay: 0;
}

.most-visited .favicon {
  background: no-repeat 5px 50%;
  bottom: 7px;
  box-sizing: border-box;
  display: block;
  height: 16px;
  position: absolute;
  width: 100%;
}

.most-visited .color-stripe {
  border-bottom-left-radius: 3px 3px;
  border-bottom-right-radius: 3px 3px;
  /* Matches height of title.  */
  bottom: 23px;
  height: 3px;
  /* Matches padding-top of the title. */
  margin-bottom: 8px;
  position: absolute;
  width: 100%;
  z-index: 100;
}

.most-visited .title {
  display: block;
  height: 23px;
  overflow: hidden;
  padding-top: 8px;
  text-align: center;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.thumbnail {
  background: no-repeat;
  background-size: 100%;
  border-radius: 3px;
  /* These max dimensions are not necessary, as the sizing logic in the .js
   * should be sufficient, but they're here for extra insurance. We never want
   * to scale a thumbnail larger than this size. */
  max-height: 132px;
  max-width: 212px;
  -webkit-transition: opacity 0.15s;
}

.filler .thumbnail {
  /* TODO(estade): there seems to be a webkit bug where this border is not
   * always removed when it should be. Investigate. */
  border: 1px solid;
  visibility: visible;
}

.thumbnail-shield {
  border-radius: 3px;
  background: -webkit-linear-gradient(rgba(255, 255, 255, 0),
                                      rgba(255, 255, 255, 0) 50%,
                                      rgba(255, 255, 255, 0.9));
}

.most-visited:hover .thumbnail {
  opacity: 0.95;
}

.most-visited:hover .thumbnail-shield,
.most-visited:active .thumbnail-shield {
  background: -webkit-linear-gradient(rgba(255, 255, 255, 0),
                                      rgba(255, 255, 255, 0) 80%,
                                      rgba(255, 255, 255, 0.9));
}

/* The thumbnail gets lighter when clicked, but not when the click is on the
 * close button. */
.most-visited:active .close-button:not(:active) + .thumbnail {
  opacity: 0.9;
}

/* The thumbnail gets a shadow when clicked, but not when the click is on the
 * close button. */
.most-visited:active .close-button:not(:active) + .thumbnail .thumbnail-shield {
  -webkit-box-shadow: inset 0 1px 10px rgba(0, 0, 0, 0.2);
}

.thumbnail-wrapper {
  border: 1px solid transparent;
  border-radius: 3px;
  display: block;
  position: relative;
  z-index: 5;
  -webkit-box-flex: 1;
  -webkit-transition: background-color .15s;
}

.filler .thumbnail-wrapper {
  visibility: visible;
}

/* 'finishing-drag' is the state we are in after dropping on the trash can.
 * Override opacity of the tile to 1, so that the new tile animation
 * occurs simultaneously with the trash animation. */
.tile.dragging.finishing-drag {
  opacity: 1;
}

/* Don't display the new tile until there's something to show.  */
.blacklisted {
  opacity: 0;
}
</style>
<style>/* Copyright (c) 2011 The Chromium Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

/* TODO(estade): handle overflow better? I tried overflow-x: hidden and
   overflow-y: visible (for the new dot animation), but this makes a scroll
   bar appear */
#dot-list {
  display: -webkit-box;
  height: 100%;
  list-style-type: none;
  margin: 0;
  padding: 0;
  /* Expand to take up all available horizontal space.  */
  -webkit-box-flex: 1;
  /* Center child dots. */
  -webkit-box-pack: center;
}

html.starting-up #dot-list {
  display: none;
}

.dot {
  box-sizing: border-box;
  cursor: pointer;
  max-width: 80px;
  outline: none;
  text-align: left;
  -webkit-box-flex: 1;
  -webkit-margin-end: 10px;
  -webkit-padding-start: 2px;
  -webkit-transition: max-width 0.25s, -webkit-margin-end 0.25s;
}

.dot:last-child {
  -webkit-margin-end: 0;
}

.dot.small {
  max-width: 0;
  -webkit-margin-end: 0;
}

.dot .selection-bar {
  border-bottom: 5px solid;
  border-color: rgba(0, 0, 0, 0.1);
  height: 10px;
  -webkit-transition: border-color 0.2s;
}

.dot input {
  background-color: transparent;
  cursor: inherit;
  /* TODO(estade): the font needs tweaking. */
  font-size: 8pt;
  font-weight: bold;
  padding-top: 0;
  width: 90%;
  -webkit-appearance: caret;
  -webkit-transition: color 0.2s;
}

.dot input:focus {
  cursor: auto;
}

/* Everything below here should be themed but we don't have appropriate colors
 * yet.
 */
.dot input {
  color: #b2b2b2;
}

.dot:focus input, .dot:hover input, .dot.selected input {
  color: #7f7f7f;
}

.dot:focus .selection-bar,
.dot:hover .selection-bar,
.dot.drag-target .selection-bar {
  border-color: #b2b2b2;
}

.dot.selected .selection-bar {
  border-color: #7f7f7f;
}
</style>
<style>/* Copyright (c) 2011 The Chromium Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

html {
  font-family: segoe ui, arial, helvetica, sans-serif;
  font-size: 14px;
  /* It's necessary to put this here instead of in body in order to get the
     background-size of 100% to work properly */
  height: 100%;
  overflow: hidden;
}

body {
  background-size: auto 100%;
  margin: 0;
  /* Don't highlight links when they're tapped.  Safari has bugs here that
     show up as flicker when dragging in some situations */
  -webkit-tap-highlight-color: transparent;
  /* Don't allow selecting text - can occur when dragging */
  -webkit-user-select: none;
}

/* [hidden] does display:none, but its priority is too low in some cases. */
[hidden] {
  display: none !important;
}

#notification-container {
  display: block;
  margin-top: 2px;
  position: relative;
  text-align: center;
  z-index: 100;
  -webkit-transition-duration: 0.1s;
  -webkit-transition-property: opacity;
}

#notification-container.inactive {
  opacity: 0;
  -webkit-transition-duration: 0.2s;
}

#notification {
  background-color: #FFF199;
  border: 1px solid lightGrey;
  border-radius: 6px;
  color: black;
  display: inline-block;
  font-weight: bold;
  padding: 7px 15px;
}

#notification > div > div,
#notification > div {
  display: inline-block;
}

#notification .close-button {
  vertical-align: middle;
  -webkit-margin-start: 0.5em;
}

.close-button {
  background: no-repeat;
  background-color: transparent;
  /* TODO(estade): this should animate between states. */
  background-image: url('chrome://theme/IDR_CLOSE_BAR');
  border: 0;
  cursor: default;
  display: inline-block;
  height: 16px;
  padding: 0;
  width: 16px;
}

.close-button:hover,
.close-button:focus {
  background-image: url('chrome://theme/IDR_CLOSE_BAR_H');
}

.close-button:active {
  background-image: url('chrome://theme/IDR_CLOSE_BAR_P');
}

.linkButton {
  color: #06C;
  cursor: pointer;
  display: inline-block;
  text-decoration: underline;
  -webkit-margin-start: 0.5em;
}

#card-slider-frame {
  /* Must match #footer height. */
  bottom: 50px;
  overflow: hidden;
  /* We want this to fill the window except for the region used
     by footer */
  position: fixed;
  top: 0;
  width: 100%;
}

#page-list {
  /* fill the apps-frame */
  height: 100%;
  display: -webkit-box;
}

#attribution {
  bottom: 0;
  left: auto;
  margin-left: 8px;
  /* Leave room for the scrollbar. */
  margin-right: 13px;
  position: absolute;
  right: 0;
  text-align: left;
  z-index: -5;
}

/* For themes that right-align their images, we flip the attribution to the
 * left to avoid conflicts.  */
html[themegravity='right'] #attribution,
html[dir='rtl'] #attribution {
  left: 0;
  right: auto;
  text-align: right;
}

#attribution > span {
  display: block;
  font-size: 84%;
}

#footer {
  background-image: -webkit-linear-gradient(
      rgba(242, 242, 242, 0.9), rgba(222, 222, 222, 0.9));
  bottom: 0;
  position: fixed;
  width: 100%;
  z-index: 5;
}

/* TODO(estade): remove this border hack and replace with a webkit-gradient
 * border-image on #footer once WebKit supports border-image-slice.
 * See https://bugs.webkit.org/show_bug.cgi?id=20127 */
#footer-border {
  height: 1px;
}

#footer-content {
  display: -webkit-box;
  height: 49px;
  -webkit-box-align: center;
}

#footer-content > * {
  margin: 0 9px;
}

#logo-img {
  margin-top: 4px;
}

.starting-up * {
  -webkit-transition: none !important;
}

/* Trash. *********************************************************************/

#trash {
  height: 100%;
  position: absolute;
  opacity: 0;
  right: 0;
  top: 50px;
  -webkit-transition: top 0.2s, opacity 0;
  -webkit-transition-delay: 0, 0.2s;
}

#footer.showing-trash-mode #trash {
  opacity: 1;
  top: 0;
  -webkit-transition-delay: 0, 0;
}

#trash > span {
  background-image: url("");
  background-position: 7px;
  background-repeat: no-repeat;
  border: 1px dashed #7f7f7f;
  border-radius: 4px;
  display: inline-block;
  font-size: 84%;
  padding-bottom: 9px;
  padding-top: 10px;
  position: relative;
  top: 7px;
  -webkit-padding-end: 7px;
  -webkit-padding-start: 30px;
}

/* Page switcher buttons. *****************************************************/

.page-switcher {
  background-color: transparent;
  border: none;
  bottom: 0;
  font-size: 40px;
  padding: 0;
  position: absolute;
  margin: 0;
  max-width: 150px;
  min-width: 90px;
  top: 0;
  z-index: 5;
  -webkit-transition: width 150ms, right 150ms, background-color 150ms;
}

.page-switcher:hover,
.page-switcher.drag-target {
  background-color: rgba(0, 0, 0, 0.2);
}

html[dir=ltr] #page-switcher-start,
html[dir=rtl] #page-switcher-end {
  border-top-right-radius: 10px;
}

html[dir=rtl] #page-switcher-start,
html[dir=ltr] #page-switcher-end {
  border-top-left-radius: 10px;
}
</style>
<style>/* Copyright (c) 2011 The Chromium Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#recently-closed-menu-button {
  background: none;
  border: 0;
  color: #7F7F7F;
  cursor: pointer;
  display: block;
  font-size: 9pt;
  font-weight: bold;
  /* The padding increases the clickable area. */
  padding-bottom: 8px;
  padding-top: 8px;
  -webkit-appearance: none;
  -webkit-transition: opacity 0.2s;
  -webkit-transition-delay: 0.1s;
}

/* Reserve space for the menu button even when it's hidden. */
#recently-closed-menu-button.invisible,
#footer.showing-trash-mode #recently-closed-menu-button {
  visibility: hidden;
}

#footer.showing-trash-mode #recently-closed-menu-button {
  opacity: 0;
  -webkit-transition-delay: 0;
}

.recent-menu {
  padding: 6px 8px;
  /* Needs to be above #footer. */
  z-index: 10;
}

.recent-menu-item {
  background: no-repeat 0% 50%;
  background-color: transparent !important;
  background-size: 16px 16px;
  box-sizing: border-box;
  display: block;
  font-size: 100%;
  line-height: 20px;
  margin: 8px;
  max-width: 450px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  -webkit-margin-start: 0;
  -webkit-padding-end: 0;
  -webkit-padding-start: 22px;
}

.recent-menu-item:not(:hover) {
  text-decoration: none;
}

.recent-menu-item:first-of-type {
  margin-top: 4px;
}

.recent-menu-item:last-of-type {
  margin-bottom: 4px;
}

html[dir='rtl'] .recent-menu-item {
  background: no-repeat 100% 50%;
}

.recent-window {
  background-image: url("");
}

/* TODO(estade): find a better color for active. */
.recent-menu-item:active,
.recent-menu-item:visited,
.recent-menu-item:link {
  color: hsl(213, 90%, 24%) !important;
}

.disclosure-triangle {
  background-color: #7F7F7F;
  display: inline-block;
  height: 9px;
  width: 9px;
  -webkit-mask-image: url("");
}
</style>
<style>/* Copyright (c) 2011 The Chromium Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

.tile-page {
  display: -webkit-box;
  height: 100%;
  position: relative;
  width: 100%;
  -webkit-box-orient: vertical;
}

.tile-page-scrollbar {
  margin: 0 4px;
  pointer-events: none;
  position: absolute;
  right: 0;
  width: 5px;
  z-index: 5;
  -webkit-box-sizing: border-box;
}

.tile-page-content {
  overflow-y: scroll;
  padding-top: 100px;
  position: relative;
  text-align: center;
  width: 100%;
  /* TODO(estade): this mask is disabled for technical reasons. It negatively
   * impacts performance of page switching, also it causes problems with Mac
   * text: http://crbug.com/86955
  -webkit-mask-image: -webkit-linear-gradient(bottom, transparent, black 30px);
  */
  /* The following four properties are necessary so that the mask won't clip
   * the scrollbar. */
  box-sizing: border-box;
  /* Scrollbar width(13px) + balance right padding.  */
  padding-left: 93px;
  padding-right: 80px;
  -webkit-box-flex: 1;
  /* Don't apply clip mask to padding. */
  -webkit-mask-clip: content-box;
}

.tile-grid {
  overflow: hidden;
  position: relative;
  width: 100%;
}

.tile {
  display: inline-block;
  position: absolute;
  /* Don't offer the context menu on long-press. */
  -webkit-touch-callout: none;
  -webkit-user-drag: element;
}

/* I don't know why this is necessary. -webkit-user-drag: element on .tile
 * should be enough. If we don't do this, we get 2 drag representations for
 * the image. */
.tile img {
  -webkit-user-drag: none;
}

.doppleganger {
  left: 0 !important;
  right: 0 !important;
  top: 0 !important;
}

.tile.dragging {
  opacity: 0;
}

.tile.drag-representation {
  pointer-events: none;
  position: fixed;
  z-index: 3;
  -webkit-transition: opacity 0.2s;
}

.tile.drag-representation.placing > * {
  -webkit-transition: -webkit-transform 200ms;
}

/* When a drag finishes while we're not showing the page where the tile
 * belongs, the tile shrinks to a dot. */
.tile.drag-representation.dropped-on-other-page > * {
   -webkit-transform: scale(0) rotate(0);
}

.tile.drag-representation.deleting > * {
  -webkit-transform: scale(0) rotate(360deg);
  -webkit-transition: -webkit-transform 600ms;
}

.animating-tile-page .tile,
.tile.drag-representation.placing {
  -webkit-transition: left 200ms, right 200ms, top 200ms;
}

.hovering-on-trash {
  opacity: 0.6;
}

.animating-tile-page .top-margin {
  -webkit-transition: margin-bottom 200ms;
}

@-webkit-keyframes bounce {
  0% {
    -webkit-transform: scale(0, 0);
  }

  60% {
    -webkit-transform: scale(1.2, 1.2);
  }

  100% {
    -webkit-transform: scale(1, 1);
  }
}

.tile > .new-tile-contents {
  -webkit-animation: bounce 0.5s ease-in-out;
}

@-webkit-keyframes blipout {
  0% {
    -webkit-transform: scale(1, 1);
  }

  60% {
    opacity: 1;
    -webkit-animation-timing-function: ease-in;
    -webkit-transform: scale(1.3, 0.02);
  }

  90% {
    opacity: 0.7;
    -webkit-animation-timing-function: default;
    -webkit-transform: scale(0.3, 0.02);
  }

  100% {
    opacity: 0;
    -webkit-animation-timing-function: linear;
    -webkit-transform: scale(0.3, 0.02);
  }
}

.tile > .removing-tile-contents {
  pointer-events: none;
  -webkit-animation: blipout 0.3s;
}

.tile-page:not(.selected-card) * {
  -webkit-transition: none !important;
}

/** Scrollbars ****************************************************************/

.tile-page-content::-webkit-scrollbar {
  width: 13px;
}

.tile-page-content::-webkit-scrollbar-button {
  display: none;
}
</style>
<link id="themecss" rel="stylesheet">

<script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/** @fileoverview EventTracker is a simple class that manages the addition and
 *  removal of DOM event listeners. In particular, it keeps track of all
 *  listeners that have been added and makes it easy to remove some or all of
 *  them without requiring all the information again. This is particularly
 *  handy when the listener is a generated function such as a lambda or the
 *  result of calling Function.bind.
 */

// Use an anonymous function to enable strict mode just for this file (which
// will be concatenated with other files when embedded in Chrome)
var EventTracker = (function() {
  'use strict';

  /**
   *  Create an EventTracker to track a set of events.
   *  EventTracker instances are typically tied 1:1 with other objects or
   *  DOM elements whose listeners should be removed when the object is disposed
   *  or the corresponding elements are removed from the DOM.
   *  @constructor
   */
  function EventTracker() {
    /**
     *  @type {Array.<EventTracker.Entry>}
     *  @private
     */
    this.listeners_ = [];
  }

  /**
   * The type of the internal tracking entry.
   *  @typedef {{node: !Node,
   *            eventType: string,
   *            listener: Function,
   *            capture: boolean}}
   */
  EventTracker.Entry;

  EventTracker.prototype = {
    /**
     * Add an event listener - replacement for Node.addEventListener.
     * @param {!Node} node The DOM node to add a listener to.
     * @param {string} eventType The type of event to subscribe to.
     * @param {Function} listener The listener to add.
     * @param {boolean} capture Whether to invoke during the capture phase.
     */
    add: function(node, eventType, listener, capture) {
      var h = {
        node: node,
        eventType: eventType,
        listener: listener,
        capture: capture
      };
      this.listeners_.push(h);
      node.addEventListener(eventType, listener, capture);
    },

    /**
     * Remove any specified event listeners added with this EventTracker.
     * @param {!Node} node The DOM node to remove a listener from.
     * @param {?string} eventType The type of event to remove.
     */
    remove: function(node, eventType) {
      this.listeners_ = this.listeners_.filter(function(h) {
        if (h.node == node && (!eventType || (h.eventType == eventType))) {
          EventTracker.removeEventListener_(h);
          return false;
        }
        return true;
      });
    },

    /**
     * Remove all event listeners added with this EventTracker.
     */
    removeAll: function() {
      this.listeners_.forEach(EventTracker.removeEventListener_);
      this.listeners_ = [];
    }
  };

  /**
   * Remove a single event listener given it's tracker entry.  It's up to the
   * caller to ensure the entry is removed from listeners_.
   * @param {EventTracker.Entry} h The entry describing the listener to remove.
   * @private
   */
  EventTracker.removeEventListener_ = function(h) {
    h.node.removeEventListener(h.eventType, h.listener, h.capture);
  };

  return EventTracker;
})();

</script>
<script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// TODO(arv): Namespace

/**
 * The local strings get injected into the page using a variable named
 * {@code templateData}. This class provides a simpler interface to access those
 * strings.
 *
 * @param {Object} opt_templateData Optional object containing translated
 *     strings.  If this is not supplied during construction, it can be
 *     assigned to the templateData property after construction.  If all else
 *     fails, the value of window.templateDate will be used.
 * @constructor
 */
function LocalStrings(opt_templateData) {
  this.templateData = opt_templateData;
}

// Start of anonymous namespace.
(function() {

/**
 * Returns a formatted string where $1 to $9 are replaced by the second to the
 * tenth argument.
 * @param {string} s The format string.
 * @param {...string} The extra values to include in the formatted output.
 * @return {string} The string after format substitution.
 */
function replaceArgs(s, args) {
  return s.replace(/\$[$1-9]/g, function(m) {
    return (m == '$$') ? '$' : args[m[1]];
  });
}

/**
 * Returns a string after removing Windows-style accelerators.
 * @param {string} s The input string that may contain accelerators.
 * @return {string} The resulting string with accelerators removed.
 */
function trimAccelerators(s) {
  return s.replace(/&{1,2}/g, function(m) {
    return (m == '&&') ? '&' : '';
  });
}

LocalStrings.prototype = {
  /**
   * The template data object.
   * @type {Object}
   */
  templateData: null,

  /**
   * Gets a localized string by its id.
   * @param {string} s The ID of the string we want.
   * @return {string} The localized string.
   */
  getString: function(id) {
    // TODO(arv): We should not rely on a global variable here.
    return (this.templateData || window.templateData)[id] || '';
  },

  /**
   * Returns a formatted localized string where $1 to $9 are replaced by the
   * second to the tenth argument.
   * @param {string} id The ID of the string we want.
   * @param {...string} The extra values to include in the formatted output.
   * @return {string} The formatted string.
   */
  getStringF: function(id, var_args) {
    return replaceArgs(this.getString(id), arguments);
  },
};

// End of anonymous namespace.
})();
</script>
<script>/**
 * Whitelist of tag names allowed in parseHtmlSubset.
 * @type {[string]}
 */
var allowedTags = ['A', 'B', 'STRONG'];

/**
 * Parse a very small subset of HTML.
 * @param {string} s The string to parse.
 * @throws {Error} In case of non supported markup.
 * @return {DocumentFragment} A document fragment containing the DOM tree.
 */
var allowedAttributes = {
  'href': function(node, value) {
    // Only allow a[href] starting with http:// and https://
    return node.tagName == 'A' && (value.indexOf('http://') == 0 ||
        value.indexOf('https://') == 0);
  },
  'target': function(node, value) {
    // Allow a[target] but reset the value to "".
    if (node.tagName != 'A')
      return false;
    node.setAttribute('target', '');
    return true;
  }
}

/**
 * Parse a very small subset of HTML.  This ensures that insecure HTML /
 * javascript cannot be injected into the new tab page.
 * @param {string} s The string to parse.
 * @throws {Error} In case of non supported markup.
 * @return {DocumentFragment} A document fragment containing the DOM tree.
 */
function parseHtmlSubset(s) {
  function walk(n, f) {
    f(n);
    for (var i = 0; i < n.childNodes.length; i++) {
      walk(n.childNodes[i], f);
    }
  }

  function assertElement(node) {
    if (allowedTags.indexOf(node.tagName) == -1)
      throw Error(node.tagName + ' is not supported');
  }

  function assertAttribute(attrNode, node) {
    var n = attrNode.nodeName;
    var v = attrNode.nodeValue;
    if (!allowedAttributes.hasOwnProperty(n) || !allowedAttributes[n](node, v))
      throw Error(node.tagName + '[' + n + '="' + v + '"] is not supported');
  }

  var r = document.createRange();
  r.selectNode(document.body);
  // This does not execute any scripts.
  var df = r.createContextualFragment(s);
  walk(df, function(node) {
    switch (node.nodeType) {
      case Node.ELEMENT_NODE:
        assertElement(node);
        var attrs = node.attributes;
        for (var i = 0; i < attrs.length; i++) {
          assertAttribute(attrs[i], node);
        }
        break;

      case Node.COMMENT_NODE:
      case Node.DOCUMENT_FRAGMENT_NODE:
      case Node.TEXT_NODE:
        break;

      default:
        throw Error('Node type ' + node.nodeType + ' is not supported');
    }
  });
  return df;
}
</script>
<script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * The global object.
 * @type {!Object}
 */
const global = this;

// TODO(estade): This should be removed and calls replaced with cr.isMac
const IS_MAC = /^Mac/.test(navigator.platform);

/**
 * Alias for document.getElementById.
 * @param {string} id The ID of the element to find.
 * @return {HTMLElement} The found element or null if not found.
 */
function $(id) {
  return document.getElementById(id);
}

/**
 * Calls chrome.send with a callback and restores the original afterwards.
 * @param {string} name The name of the message to send.
 * @param {!Array} params The parameters to send.
 * @param {string} callbackName The name of the function that the backend calls.
 * @param {!Function} The function to call.
 */
function chromeSend(name, params, callbackName, callback) {
  var old = global[callbackName];
  global[callbackName] = function() {
    // restore
    global[callbackName] = old;

    var args = Array.prototype.slice.call(arguments);
    return callback.apply(global, args);
  };
  chrome.send(name, params);
}

/**
 * Generates a CSS url string.
 * @param {string} s The URL to generate the CSS url for.
 * @return {string} The CSS url string.
 */
function url(s) {
  // http://www.w3.org/TR/css3-values/#uris
  // Parentheses, commas, whitespace characters, single quotes (') and double
  // quotes (") appearing in a URI must be escaped with a backslash
  var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1');
  // WebKit has a bug when it comes to URLs that end with \
  // https://bugs.webkit.org/show_bug.cgi?id=28885
  if (/\\\\$/.test(s2)) {
    // Add a space to work around the WebKit bug.
    s2 += ' ';
  }
  return 'url("' + s2 + '")';
}

/**
 * Parses query parameters from Location.
 * @param {string} s The URL to generate the CSS url for.
 * @return {object} Dictionary containing name value pairs for URL
 */
function parseQueryParams(location) {
  var params = {};
  var query = unescape(location.search.substring(1));
  var vars = query.split("&");
  for (var i=0; i < vars.length; i++) {
    var pair = vars[i].split("=");
    params[pair[0]] = pair[1];
  }
  return params;
}

function findAncestorByClass(el, className) {
  return findAncestor(el, function(el) {
    if (el.classList)
      return el.classList.contains(className);
    return null;
  });
}

/**
 * Return the first ancestor for which the {@code predicate} returns true.
 * @param {Node} node The node to check.
 * @param {function(Node) : boolean} predicate The function that tests the
 *     nodes.
 * @return {Node} The found ancestor or null if not found.
 */
function findAncestor(node, predicate) {
  var last = false;
  while (node != null && !(last = predicate(node))) {
    node = node.parentNode;
  }
  return last ? node : null;
}

function swapDomNodes(a, b) {
  var afterA = a.nextSibling;
  if (afterA == b) {
    swapDomNodes(b, a);
    return;
  }
  var aParent = a.parentNode;
  b.parentNode.replaceChild(a, b);
  aParent.insertBefore(b, afterA);
}

/**
 * Disables text selection and dragging.
 */
function disableTextSelectAndDrag() {
  // Disable text selection.
  document.onselectstart = function(e) {
    e.preventDefault();
  }

  // Disable dragging.
  document.ondragstart = function(e) {
    e.preventDefault();
  }
}

// Handle click on a link. If the link points to a chrome: or file: url, then
// call into the browser to do the navigation.
document.addEventListener('click', function(e) {
  // Allow preventDefault to work.
  if (!e.returnValue)
    return;

  var el = e.target;
  if (el.nodeType == Node.ELEMENT_NODE &&
      el.webkitMatchesSelector('A, A *')) {
    while (el.tagName != 'A') {
      el = el.parentElement;
    }

    if ((el.protocol == 'file:' || el.protocol == 'about:') &&
        (e.button == 0 || e.button == 1)) {
      chrome.send('navigateToUrl', [
        el.href,
        el.target,
        e.button,
        e.altKey,
        e.ctrlKey,
        e.metaKey,
        e.shiftKey
      ]);
      e.preventDefault();
    }
  }
});
</script>

<script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

const cr = (function() {

  /**
   * Whether we are using a Mac or not.
   * @type {boolean}
   */
  const isMac = /Mac/.test(navigator.platform);

  /**
   * Whether this is on the Windows platform or not.
   * @type {boolean}
   */
  const isWindows = /Win/.test(navigator.platform);

  /**
   * Whether this is on chromeOS or not.
   * @type {boolean}
   */
  const isChromeOS = /CrOS/.test(navigator.userAgent);

  /**
   * Whether this is on touchui build or not.
   * @type {boolean}
   */
  const isTouch = /Touch/.test(navigator.userAgent);

  /**
   * Whether this is on vanilla Linux (not chromeOS).
   * @type {boolean}
   */
  const isLinux = /Linux/.test(navigator.userAgent);

  /**
   * Whether this uses the views toolkit or not.
   * @type {boolean}
   */
  const isViews = isWindows || isChromeOS;

  /**
   * Sets the os and toolkit attributes in the <html> element so that platform
   * specific css rules can be applied.
   */
  function enablePlatformSpecificCSSRules() {
    if (isMac)
      doc.documentElement.setAttribute('os', 'mac');
    if (isWindows)
      doc.documentElement.setAttribute('os', 'windows');
    if (isChromeOS)
      doc.documentElement.setAttribute('os', 'chromeos');
    if (isLinux) {
      doc.documentElement.setAttribute('os', 'linux');
      doc.documentElement.setAttribute('toolkit', 'gtk');
    }
    if (isViews)
      doc.documentElement.setAttribute('toolkit', 'views');
  }

  /**
   * Builds an object structure for the provided namespace path,
   * ensuring that names that already exist are not overwritten. For
   * example:
   * "a.b.c" -> a = {};a.b={};a.b.c={};
   * @param {string} name Name of the object that this file defines.
   * @param {*=} opt_object The object to expose at the end of the path.
   * @param {Object=} opt_objectToExportTo The object to add the path to;
   *     default is {@code window}.
   * @private
   */
  function exportPath(name, opt_object, opt_objectToExportTo) {
    var parts = name.split('.');
    var cur = opt_objectToExportTo || window /* global */;

    for (var part; parts.length && (part = parts.shift());) {
      if (!parts.length && opt_object !== undefined) {
        // last part and we have an object; use it
        cur[part] = opt_object;
      } else if (part in cur) {
        cur = cur[part];
      } else {
        cur = cur[part] = {};
      }
    }
    return cur;
  };

  // cr.Event is called CrEvent in here to prevent naming conflicts. We also
  // store the original Event in case someone does a global alias of cr.Event.
  const DomEvent = Event;

  /**
   * Creates a new event to be used with cr.EventTarget or DOM EventTarget
   * objects.
   * @param {string} type The name of the event.
   * @param {boolean=} opt_bubbles Whether the event bubbles. Default is false.
   * @param {boolean=} opt_preventable Whether the default action of the event
   *     can be prevented.
   * @constructor
   * @extends {DomEvent}
   */
  function CrEvent(type, opt_bubbles, opt_preventable) {
    var e = cr.doc.createEvent('Event');
    e.initEvent(type, !!opt_bubbles, !!opt_preventable);
    e.__proto__ = CrEvent.prototype;
    return e;
  }

  CrEvent.prototype = {
    __proto__: DomEvent.prototype
  };

  /**
   * Fires a property change event on the target.
   * @param {EventTarget} target The target to dispatch the event on.
   * @param {string} propertyName The name of the property that changed.
   * @param {*} newValue The new value for the property.
   * @param {*} oldValue The old value for the property.
   */
  function dispatchPropertyChange(target, propertyName, newValue, oldValue) {
    var e = new CrEvent(propertyName + 'Change');
    e.propertyName = propertyName;
    e.newValue = newValue;
    e.oldValue = oldValue;
    target.dispatchEvent(e);
  }

  /**
   * The kind of property to define in {@code defineProperty}.
   * @enum {number}
   */
  const PropertyKind = {
    /**
     * Plain old JS property where the backing data is stored as a "private"
     * field on the object.
     */
    JS: 'js',

    /**
     * The property backing data is stored as an attribute on an element.
     */
    ATTR: 'attr',

    /**
     * The property backing data is stored as an attribute on an element. If the
     * element has the attribute then the value is true.
     */
    BOOL_ATTR: 'boolAttr'
  };

  /**
   * Helper function for defineProperty that returns the getter to use for the
   * property.
   * @param {string} name
   * @param {cr.PropertyKind} kind
   * @return {function():*} The getter for the property.
   */
  function getGetter(name, kind) {
    switch (kind) {
      case PropertyKind.JS:
        var privateName = name + '_';
        return function() {
          return this[privateName];
        };
      case PropertyKind.ATTR:
        return function() {
          return this.getAttribute(name);
        };
      case PropertyKind.BOOL_ATTR:
        return function() {
          return this.hasAttribute(name);
        };
    }
  }

  /**
   * Helper function for defineProperty that returns the setter of the right
   * kind.
   * @param {string} name The name of the property we are defining the setter
   *     for.
   * @param {cr.PropertyKind} kind The kind of property we are getting the
   *     setter for.
   * @param {function(*):void} opt_setHook A function to run after the property
   *     is set, but before the propertyChange event is fired.
   * @return {function(*):void} The function to use as a setter.
   */
  function getSetter(name, kind, opt_setHook) {
    switch (kind) {
      case PropertyKind.JS:
        var privateName = name + '_';
        return function(value) {
          var oldValue = this[privateName];
          if (value !== oldValue) {
            this[privateName] = value;
            if (opt_setHook)
              opt_setHook.call(this, value, oldValue);
            dispatchPropertyChange(this, name, value, oldValue);
          }
        };

      case PropertyKind.ATTR:
        return function(value) {
          var oldValue = this[name];
          if (value !== oldValue) {
            if (value == undefined)
              this.removeAttribute(name);
            else
              this.setAttribute(name, value);
            if (opt_setHook)
              opt_setHook.call(this, value, oldValue);
            dispatchPropertyChange(this, name, value, oldValue);
          }
        };

      case PropertyKind.BOOL_ATTR:
        return function(value) {
          var oldValue = this[name];
          if (value !== oldValue) {
            if (value)
              this.setAttribute(name, name);
            else
              this.removeAttribute(name);
            if (opt_setHook)
              opt_setHook.call(this, value, oldValue);
            dispatchPropertyChange(this, name, value, oldValue);
          }
        };
    }
  }

  /**
   * Defines a property on an object. When the setter changes the value a
   * property change event with the type {@code name + 'Change'} is fired.
   * @param {!Object} obj The object to define the property for.
   * @param {string} name The name of the property.
   * @param {cr.PropertyKind=} opt_kind What kind of underlying storage to use.
   * @param {function(*):void} opt_setHook A function to run after the
   *     property is set, but before the propertyChange event is fired.
   */
  function defineProperty(obj, name, opt_kind, opt_setHook) {
    if (typeof obj == 'function')
      obj = obj.prototype;

    var kind = opt_kind || PropertyKind.JS;

    if (!obj.__lookupGetter__(name)) {
      obj.__defineGetter__(name, getGetter(name, kind));
    }

    if (!obj.__lookupSetter__(name)) {
      obj.__defineSetter__(name, getSetter(name, kind, opt_setHook));
    }
  }

  /**
   * Counter for use with createUid
   */
  var uidCounter = 1;

  /**
   * @return {number} A new unique ID.
   */
  function createUid() {
    return uidCounter++;
  }

  /**
   * Returns a unique ID for the item. This mutates the item so it needs to be
   * an object
   * @param {!Object} item The item to get the unique ID for.
   * @return {number} The unique ID for the item.
   */
  function getUid(item) {
    if (item.hasOwnProperty('uid'))
      return item.uid;
    return item.uid = createUid();
  }

  /**
   * Dispatches a simple event on an event target.
   * @param {!EventTarget} target The event target to dispatch the event on.
   * @param {string} type The type of the event.
   * @param {boolean=} opt_bubbles Whether the event bubbles or not.
   * @param {boolean=} opt_cancelable Whether the default action of the event
   *     can be prevented.
   * @return {boolean} If any of the listeners called {@code preventDefault}
   *     during the dispatch this will return false.
   */
  function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) {
    var e = new cr.Event(type, opt_bubbles, opt_cancelable);
    return target.dispatchEvent(e);
  }

  /**
   * @param {string} name
   * @param {!Function} fun
   */
  function define(name, fun) {
    var obj = exportPath(name);
    var exports = fun();
    for (var propertyName in exports) {
      // Maybe we should check the prototype chain here? The current usage
      // pattern is always using an object literal so we only care about own
      // properties.
      var propertyDescriptor = Object.getOwnPropertyDescriptor(exports,
                                                               propertyName);
      if (propertyDescriptor)
        Object.defineProperty(obj, propertyName, propertyDescriptor);
    }
  }

  /**
   * Document used for various document related operations.
   * @type {!Document}
   */
  var doc = document;


  /**
   * Allows you to run func in the context of a different document.
   * @param {!Document} document The document to use.
   * @param {function():*} func The function to call.
   */
  function withDoc(document, func) {
    var oldDoc = doc;
    doc = document;
    try {
      func();
    } finally {
      doc = oldDoc;
    }
  }

  /**
   * Adds a {@code getInstance} static method that always return the same
   * instance object.
   * @param {!Function} ctor The constructor for the class to add the static
   *     method to.
   */
  function addSingletonGetter(ctor) {
    ctor.getInstance = function() {
      return ctor.instance_ || (ctor.instance_ = new ctor());
    };
  }

  return {
    addSingletonGetter: addSingletonGetter,
    isChromeOS: isChromeOS,
    isMac: isMac,
    isWindows: isWindows,
    isLinux: isLinux,
    isViews: isViews,
    isTouch: isTouch,
    enablePlatformSpecificCSSRules: enablePlatformSpecificCSSRules,
    define: define,
    defineProperty: defineProperty,
    PropertyKind: PropertyKind,
    createUid: createUid,
    getUid: getUid,
    dispatchSimpleEvent: dispatchSimpleEvent,
    dispatchPropertyChange: dispatchPropertyChange,

    /**
     * The document that we are currently using.
     * @type {!Document}
     */
    get doc() {
      return doc;
    },
    withDoc: withDoc,
    Event: CrEvent
  };
})();
</script>
<script>// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

cr.define('cr.ui', function() {

  /**
   * Decorates elements as an instance of a class.
   * @param {string|!Element} source The way to find the element(s) to decorate.
   *     If this is a string then {@code querySeletorAll} is used to find the
   *     elements to decorate.
   * @param {!Function} constr The constructor to decorate with. The constr
   *     needs to have a {@code decorate} function.
   */
  function decorate(source, constr) {
    var elements;
    if (typeof source == 'string')
      elements = cr.doc.querySelectorAll(source);
    else
      elements = [source];

    for (var i = 0, el; el = elements[i]; i++) {
      if (!(el instanceof constr))
        constr.decorate(el);
    }
  }

  /**
   * Helper function for creating new element for define.
   */
  function createElementHelper(tagName, opt_bag) {
    // Allow passing in ownerDocument to create in a different document.
    var doc;
    if (opt_bag && opt_bag.ownerDocument)
      doc = opt_bag.ownerDocument;
    else
      doc = cr.doc;
    return doc.createElement(tagName);
  }

  /**
   * Creates the constructor for a UI element class.
   *
   * Usage:
   * <pre>
   * var List = cr.ui.define('list');
   * List.prototype = {
   *   __proto__: HTMLUListElement.prototype,
   *   decorate: function() {
   *     ...
   *   },
   *   ...
   * };
   * </pre>
   *
   * @param {string|Function} tagNameOrFunction The tagName or
   *     function to use for newly created elements. If this is a function it
   *     needs to return a new element when called.
   * @return {function(Object=):Element} The constructor function which takes
   *     an optional property bag. The function also has a static
   *     {@code decorate} method added to it.
   */
  function define(tagNameOrFunction) {
    var createFunction, tagName;
    if (typeof tagNameOrFunction == 'function') {
      createFunction = tagNameOrFunction;
      tagName = '';
    } else {
      createFunction = createElementHelper;
      tagName = tagNameOrFunction;
    }

    /**
     * Creates a new UI element constructor.
     * @param {Object=} opt_propertyBag Optional bag of properties to set on the
     *     object after created. The property {@code ownerDocument} is special
     *     cased and it allows you to create the element in a different
     *     document than the default.
     * @constructor
     */
    function f(opt_propertyBag) {
      var el = createFunction(tagName, opt_propertyBag);
      f.decorate(el);
      for (var propertyName in opt_propertyBag) {
        el[propertyName] = opt_propertyBag[propertyName];
      }
      return el;
    }

    /**
     * Decorates an element as a UI element class.
     * @param {!Element} el The element to decorate.
     */
    f.decorate = function(el) {
      el.__proto__ = f.prototype;
      el.decorate();
    };

    return f;
  }

  /**
   * Input elements do not grow and shrink with their content. This is a simple
   * (and not very efficient) way of handling shrinking to content with support
   * for min width and limited by the width of the parent element.
   * @param {HTMLElement} el The element to limit the width for.
   * @param {number} parentEl The parent element that should limit the size.
   * @param {number} min The minimum width.
   */
  function limitInputWidth(el, parentEl, min) {
    // Needs a size larger than borders
    el.style.width = '10px';
    var doc = el.ownerDocument;
    var win = doc.defaultView;
    var computedStyle = win.getComputedStyle(el);
    var parentComputedStyle = win.getComputedStyle(parentEl);
    var rtl = computedStyle.direction == 'rtl';

    // To get the max width we get the width of the treeItem minus the position
    // of the input.
    var inputRect = el.getBoundingClientRect();  // box-sizing
    var parentRect = parentEl.getBoundingClientRect();
    var startPos = rtl ? parentRect.right - inputRect.right :
        inputRect.left - parentRect.left;

    // Add up border and padding of the input.
    var inner = parseInt(computedStyle.borderLeftWidth, 10) +
        parseInt(computedStyle.paddingLeft, 10) +
        parseInt(computedStyle.paddingRight, 10) +
        parseInt(computedStyle.borderRightWidth, 10);

    // We also need to subtract the padding of parent to prevent it to overflow.
    var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) :
        parseInt(parentComputedStyle.paddingRight, 10);

    var max = parentEl.clientWidth - startPos - inner - parentPadding;

    function limit() {
      if (el.scrollWidth > max) {
        el.style.width = max + 'px';
      } else {
        el.style.width = 0;
        var sw = el.scrollWidth;
        if (sw < min) {
          el.style.width = min + 'px';
        } else {
          el.style.width = sw + 'px';
        }
      }
    }

    el.addEventListener('input', limit);
    limit();
  }

  return {
    decorate: decorate,
    define: define,
    limitInputWidth: limitInputWidth
  };
});
</script>
<script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// require: event_tracker.js

cr.define('cr.ui', function() {

  /**
   * Bubble is a free-floating informational bubble with a triangular arrow
   * that points at a place of interest on the page. Currently the arrow is
   * always positioned at the bottom left and points down.
   */
  var Bubble = cr.ui.define('div');

  Bubble.prototype = {
    __proto__: HTMLDivElement.prototype,

    decorate: function() {
      this.className = 'bubble';
      this.innerHTML =
          '<div class=\"bubble-contents\"></div>' +
          '<div class=\"bubble-close\"></div>' +
          '<div class=\"bubble-shadow\"></div>' +
          '<div class=\"bubble-arrow\"></div>';

      this.hidden = true;
      this.handleCloseEvent = this.hide;
    },

    /**
     * Sets the child node of the bubble.
     * @param {node} An HTML element
     */
    set content(node) {
      var bubbleContent = this.querySelector('.bubble-contents');
      bubbleContent.innerHTML = "";
      bubbleContent.appendChild(node);
    },

    /**
     * Handles close event which is triggered when the close button
     * is clicked. By default is set to this.hide.
     * @param {function} A function with no parameters
     */
    set handleCloseEvent(func) {
      this.handleCloseEvent_ = func;
    },

    /**
     * Sets the anchor node, i.e. the node that this bubble points at.
     * @param {HTMLElement} node The new anchor node.
     */
    set anchorNode(node) {
      this.anchorNode_ = node;

      if (!this.hidden)
        this.reposition();
    },

    /**
     * Updates the position of the bubble. This is automatically called when
     * the window is resized, but should also be called any time the layout
     * may have changed.
     */
    reposition: function() {
      var node = this.anchorNode_;
      var clientRect = node.getBoundingClientRect();

      this.style.left = this.style.right =
          (clientRect.left + clientRect.right) / 2 + 'px';
      this.style.top = (clientRect.top - this.clientHeight) + 'px';
    },

    /**
     * Starts showing the bubble. The bubble will grab input and show until the
     * user clicks away.
     */
    show: function() {
      if (!this.hidden)
        return;

      document.body.appendChild(this);
      this.hidden = false;
      this.reposition();

      this.eventTracker_ = new EventTracker;
      this.eventTracker_.add(window, 'resize', this.reposition.bind(this));

      var doc = this.ownerDocument;
      this.eventTracker_.add(doc, 'keydown', this, true);
      this.eventTracker_.add(doc, 'mousedown', this, true);
    },

    /**
     * Hides the bubble from view.
     */
    hide: function() {
      this.hidden = true;
      this.eventTracker_.removeAll();
      this.parentNode.removeChild(this);
    },

    /**
     * Handles keydown and mousedown events, dismissing the bubble if
     * necessary.
     * @param {Event} e The event.
     */
    handleEvent: function(e) {
      switch (e.type) {
        case 'keydown':
          if (e.keyCode == 27)  // Esc
            this.hide();
          break;

        case 'mousedown':
          if (e.target == this.querySelector('.bubble-close'))
            this.handleCloseEvent_();
          else if (!this.contains(e.target))
            this.hide();
          break;
      }

      e.stopPropagation();
      e.preventDefault();
      return;
    },
  };

  return {
    Bubble: Bubble
  };
});
</script>
<script>// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

cr.define('cr.ui', function() {

  const MenuItem = cr.ui.MenuItem;

  /**
   * Creates a new menu element.
   * @param {Object=} opt_propertyBag Optional properties.
   * @constructor
   * @extends {HTMLMenuElement}
   */
  var Menu = cr.ui.define('menu');

  Menu.prototype = {
    __proto__: HTMLMenuElement.prototype,

    selectedIndex_: -1,

    /**
     * Initializes the menu element.
     */
    decorate: function() {
      this.addEventListener('mouseover', this.handleMouseOver_);
      this.addEventListener('mouseout', this.handleMouseOut_);

      // Decorate the children as menu items.
      var children = this.children;
      for (var i = 0, child; child = children[i]; i++) {
        cr.ui.decorate(child, MenuItem);
      }
    },

    /**
     * Walks up the ancestors until a menu item belonging to this menu is found.
     * @param {Element} el
     * @return {cr.ui.MenuItem} The found menu item or null.
     * @private
     */
    findMenuItem_: function(el) {
      while (el && el.parentNode != this) {
        el = el.parentNode;
      }
      return el;
    },

    /**
     * Handles mouseover events and selects the hovered item.
     * @param {Event} e The mouseover event.
     * @private
     */
    handleMouseOver_: function(e) {
      var overItem = this.findMenuItem_(e.target);
      this.selectedItem = overItem;
    },

    /**
     * Handles mouseout events and deselects any selected item.
     * @param {Event} e The mouseout event.
     * @private
     */
    handleMouseOut_: function(e) {
      this.selectedItem = null;
    },

    /**
     * The selected menu item or null if none.
     * @type {cr.ui.MenuItem}
     */
    get selectedItem() {
      return this.children[this.selectedIndex];
    },
    set selectedItem(item) {
      var index = Array.prototype.indexOf.call(this.children, item);
      this.selectedIndex = index;
    },

    /**
     * This is the function that handles keyboard navigation. This is usually
     * called by the element responsible for managing the menu.
     * @param {Event} e The keydown event object.
     * @return {boolean} Whether the event was handled be the menu.
     */
    handleKeyDown: function(e) {
      var item = this.selectedItem;

      var self = this;
      function selectNextVisible(m) {
        var children = self.children;
        var len = children.length;
        var i = self.selectedIndex;
        if (i == -1 && m == -1) {
          // Edge case when we need to go the last item fisrt.
          i = 0;
        }
        while (true) {
          i = (i + m + len) % len;
          item = children[i];
          if (item && !item.isSeparator() && !item.hidden)
            break;
        }
        if (item)
          self.selectedIndex = i;
      }

      switch (e.keyIdentifier) {
        case 'Down':
          selectNextVisible(1);
          return true;
        case 'Up':
          selectNextVisible(-1);
          return true;
        case 'Enter':
        case 'U+0020': // Space
          if (item) {
            if (cr.dispatchSimpleEvent(item, 'activate', true, true)) {
              if (item.command)
                item.command.execute();
            }
          }
          return true;
      }

      return false;
    }
  };

  function selectedIndexChanged(selectedIndex, oldSelectedIndex) {
    var oldSelectedItem = this.children[oldSelectedIndex];
    if (oldSelectedItem)
      oldSelectedItem.selected = false;
    var item = this.selectedItem;
    if (item)
      item.selected = true;
  }
  /**
   * The selected menu item.
   * @type {number}
   */
  cr.defineProperty(Menu, 'selectedIndex', cr.PropertyKind.JS,
      selectedIndexChanged);

  // Export
  return {
    Menu: Menu
  };
});
</script>
<script>// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

cr.define('cr.ui', function() {
  const Command = cr.ui.Command;

  /**
   * Creates a new menu item element.
   * @param {Object=} opt_propertyBag Optional properties.
   * @constructor
   * @extends {HTMLDivElement}
   */
  var MenuItem = cr.ui.define('div');

  /**
   * Creates a new menu separator element.
   * @return {cr.ui.MenuItem}
   */
  MenuItem.createSeparator = function() {
    var el = cr.doc.createElement('hr');
    MenuItem.decorate(el);
    return el;
  };

  MenuItem.prototype = {
    __proto__: HTMLButtonElement.prototype,

    /**
     * Initializes the menu item.
     */
    decorate: function() {
      var commandId;
      if ((commandId = this.getAttribute('command')))
        this.command = commandId;

      this.addEventListener('mouseup', this.handleMouseUp_);
    },

    /**
     * The command associated with this menu item. If this is set to a string
     * of the form "#element-id" then the element is looked up in the document
     * of the command.
     * @type {cr.ui.Command}
     */
    command_: null,
    get command() {
      return this.command_;
    },
    set command(command) {
      if (this.command_) {
        this.command_.removeEventListener('labelChange', this);
        this.command_.removeEventListener('disabledChange', this);
        this.command_.removeEventListener('hiddenChange', this);
        this.command_.removeEventListener('checkedChange', this);
      }

      if (typeof command == 'string' && command[0] == '#') {
        command = this.ownerDocument.getElementById(command.slice(1));
        cr.ui.decorate(command, Command);
      }

      this.command_ = command;
      if (command) {
        if (command.id)
          this.setAttribute('command', '#' + command.id);

        this.label = command.label;
        this.disabled = command.disabled;
        this.hidden = command.hidden;

        this.command_.addEventListener('labelChange', this);
        this.command_.addEventListener('disabledChange', this);
        this.command_.addEventListener('hiddenChange', this);
        this.command_.addEventListener('checkedChange', this);
      }
    },

    /**
     * The text label.
     * @type {string}
     */
    get label() {
      return this.textContent;
    },
    set label(label) {
      this.textContent = label;
    },

    /**
     * @return {boolean} Whether the menu item is a separator.
     */
    isSeparator: function() {
      return this.tagName == 'HR';
    },

    /**
     * Handles mouseup events. This dispatches an active event and if there
     * is an assiciated command then that is executed.
     * @param {Event} The mouseup event object.
     * @private
     */
    handleMouseUp_: function(e) {
      if (!this.disabled && !this.isSeparator()) {
        // Dispatch command event followed by executing the command object.
        if (cr.dispatchSimpleEvent(this, 'activate', true, true)) {
          var command = this.command;
          if (command)
            command.execute();
        }
      }
    },

    /**
     * Handles changes to the associated command.
     * @param {Event} e The event object.
     */
    handleEvent: function(e) {
      switch (e.type) {
        case 'disabledChange':
          this.disabled = this.command.disabled;
          break;
        case 'hiddenChange':
          this.hidden = this.command.hidden;
          break;
        case 'labelChange':
          this.label = this.command.label;
          break;
        case 'checkedChange':
          this.checked = this.command.checked;
          break;
      }
    }
  };

  /**
   * Whether the menu item is disabled or not.
   * @type {boolean}
   */
  cr.defineProperty(MenuItem, 'disabled', cr.PropertyKind.BOOL_ATTR);

  /**
   * Whether the menu item is hidden or not.
   * @type {boolean}
   */
  cr.defineProperty(MenuItem, 'hidden', cr.PropertyKind.BOOL_ATTR);

  /**
   * Whether the menu item is selected or not.
   * @type {boolean}
   */
  cr.defineProperty(MenuItem, 'selected', cr.PropertyKind.BOOL_ATTR);

  /**
   * Whether the menu item is checked or not.
   * @type {boolean}
   */
  cr.defineProperty(MenuItem, 'checked', cr.PropertyKind.BOOL_ATTR);

  // Export
  return {
    MenuItem: MenuItem
  };
});
</script>
<script>// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview This file provides utility functions for position popups.
 */

cr.define('cr.ui', function() {

  /**
   * Type def for rects as returned by getBoundingClientRect.
   * @typedef { {left: number, top: number, width: number, height: number,
   *             right: number, bottom: number}}
   */
  var Rect;

  /**
   * Enum for defining how to anchor a popup to an anchor element.
   * @enum {number}
   */
  const AnchorType = {
    /**
     * The popup's right edge is aligned with the left edge of the anchor.
     * The popup's top edge is aligned with the top edge of the anchor.
     */
    BEFORE: 1,  // p: right, a: left, p: top, a: top

    /**
     * The popop's left edge is aligned with the right edge of the anchor.
     * The popup's top edge is aligned with the top edge of the anchor.
     */
    AFTER: 2,  // p: left a: right, p: top, a: top

    /**
     * The popop's bottom edge is aligned with the top edge of the anchor.
     * The popup's left edge is aligned with the left edge of the anchor.
     */
    ABOVE: 3,  // p: bottom, a: top, p: left, a: left

    /**
     * The popop's top edge is aligned with the bottom edge of the anchor.
     * The popup's left edge is aligned with the left edge of the anchor.
     */
    BELOW: 4  // p: top, a: bottom, p: left, a: left
  };

  /**
   * Helper function for positionPopupAroundElement and positionPopupAroundRect.
   * @param {!Rect} anchorRect The rect for the anchor.
   * @param {!HTMLElement} popupElement The element used for the popup.
   * @param {AnchorType} type The type of anchoring to do.
   * @param {boolean} invertLeftRight Whether to invert the right/left
   *     alignment.
   */
  function positionPopupAroundRect(anchorRect, popupElement, type,
                                   invertLeftRight) {
    var popupRect = popupElement.getBoundingClientRect();
    var availRect;
    var ownerDoc = popupElement.ownerDocument;
    var cs = ownerDoc.defaultView.getComputedStyle(popupElement);
    var docElement = ownerDoc.documentElement;

    if (cs.position == 'fixed') {
      // For 'fixed' positioned popups, the available rectangle should be based
      // on the viewport rather than the document.
      availRect = {
        height: docElement.clientHeight,
        width: docElement.clientWidth,
        top: 0,
        bottom: docElement.clientHeight,
        left: 0,
        right: docElement.clientWidth
      };
    } else {
      availRect = popupElement.offsetParent.getBoundingClientRect();
    }

    if (cs.direction == 'rtl')
      invertLeftRight = !invertLeftRight;

    // Flip BEFORE, AFTER based on alignment.
    if (invertLeftRight) {
      if (type == AnchorType.BEFORE)
        type = AnchorType.AFTER;
      else if (type == AnchorType.AFTER)
        type = AnchorType.BEFORE;
    }

    // Flip type based on available size
    switch (type) {
      case AnchorType.BELOW:
        if (anchorRect.bottom + popupRect.height > availRect.height &&
            popupRect.height <= anchorRect.top) {
          type = AnchorType.ABOVE;
        }
        break;
      case AnchorType.ABOVE:
        if (popupRect.height > anchorRect.top &&
            anchorRect.bottom + popupRect.height <= availRect.height) {
          type = AnchorType.BELOW;
        }
        break;
      case AnchorType.AFTER:
        if (anchorRect.right + popupRect.width > availRect.width &&
            popupRect.width <= anchorRect.left) {
          type = AnchorType.BEFORE;
        }
        break;
      case AnchorType.BEFORE:
        if (popupRect.width > anchorRect.left &&
            anchorRect.right + popupRect.width <= availRect.width) {
          type = AnchorType.AFTER;
        }
        break;
    }
    // flipping done

    var style = popupElement.style;
    // Reset all directions.
    style.left = style.right = style.top = style.bottom = 'auto'

    // Primary direction
    switch (type) {
      case AnchorType.BELOW:
        if (anchorRect.bottom + popupRect.height <= availRect.height)
          style.top = anchorRect.bottom + 'px';
        else
          style.bottom = '0';
        break;
      case AnchorType.ABOVE:
        if (availRect.height - anchorRect.top >= 0)
          style.bottom = availRect.height - anchorRect.top + 'px';
        else
          style.top = '0';
        break;
      case AnchorType.AFTER:
        if (anchorRect.right + popupRect.width <= availRect.width)
          style.left = anchorRect.right + 'px';
        else
          style.right = '0';
        break;
      case AnchorType.BEFORE:
        if (availRect.width - anchorRect.left >= 0)
          style.right = availRect.width - anchorRect.left + 'px';
        else
          style.left = '0';
        break;
    }

    // Secondary direction
    switch (type) {
      case AnchorType.BELOW:
      case AnchorType.ABOVE:
        if (invertLeftRight) {
          // align right edges
          if (anchorRect.right - popupRect.width >= 0) {
            style.right = availRect.width - anchorRect.right + 'px';

          // align left edges
          } else if (anchorRect.left + popupRect.width <= availRect.width) {
            style.left = anchorRect.left + 'px';

          // not enough room on either side
          } else {
            style.right = '0';
          }
        } else {
          // align left edges
          if (anchorRect.left + popupRect.width <= availRect.width) {
            style.left = anchorRect.left + 'px';

          // align right edges
          } else if (anchorRect.right - popupRect.width >= 0) {
            style.right = availRect.width - anchorRect.right + 'px';

          // not enough room on either side
          } else {
            style.left = '0';
          }
        }
        break;

      case AnchorType.AFTER:
      case AnchorType.BEFORE:
        // align top edges
        if (anchorRect.top + popupRect.height <= availRect.height) {
          style.top = anchorRect.top + 'px';

        // align bottom edges
        } else if (anchorRect.bottom - popupRect.height >= 0) {
          style.bottom = availRect.height - anchorRect.bottom + 'px';

          // not enough room on either side
        } else {
          style.top = '0';
        }
        break;
    }
  }

  /**
   * Positions a popup element relative to an anchor element. The popup element
   * should have position set to absolute and it should be a child of the body
   * element.
   * @param {!HTMLElement} anchorElement The element that the popup is anchored
   *     to.
   * @param {!HTMLElement} popupElement The popup element we are positioning.
   * @param {AnchorType} type The type of anchoring we want.
   * @param {boolean} invertLeftRight Whether to invert the right/left
   *     alignment.
   */
  function positionPopupAroundElement(anchorElement, popupElement, type,
                                      invertLeftRight) {
    var anchorRect = anchorElement.getBoundingClientRect();
    positionPopupAroundRect(anchorRect, popupElement, type, invertLeftRight);
  }

  /**
   * Positions a popup around a point.
   * @param {number} x The client x position.
   * @param {number} y The client y position.
   * @param {!HTMLElement} popupElement The popup element we are positioning.
   */
  function positionPopupAtPoint(x, y, popupElement) {
    var rect = {
      left: x,
      top: y,
      width: 0,
      height: 0,
      right: x,
      bottom: y
    };
    positionPopupAroundRect(rect, popupElement, AnchorType.BELOW);
  }

  // Export
  return {
    AnchorType: AnchorType,
    positionPopupAroundElement: positionPopupAroundElement,
    positionPopupAtPoint: positionPopupAtPoint
  };
});
</script>
<script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

cr.define('cr.ui', function() {
  const Menu = cr.ui.Menu;
  const positionPopupAroundElement = cr.ui.positionPopupAroundElement;

  /**
   * Creates a new menu button element.
   * @param {Object=} opt_propertyBag Optional properties.
   * @constructor
   * @extends {HTMLButtonElement}
   */
  var MenuButton = cr.ui.define('button');

  MenuButton.prototype = {
    __proto__: HTMLButtonElement.prototype,

    /**
     * Initializes the menu button.
     */
    decorate: function() {
      this.addEventListener('mousedown', this);
      this.addEventListener('keydown', this);

      var menu;
      if ((menu = this.getAttribute('menu')))
        this.menu = menu;

      // An event tracker for events we only connect to while the menu is
      // displayed.
      this.showingEvents_ = new EventTracker();

      this.anchorType = cr.ui.AnchorType.BELOW;
      this.invertLeftRight = false;
    },

    /**
     * The menu associated with the menu button.
     * @type {cr.ui.Menu}
     */
    get menu() {
      return this.menu_;
    },
    set menu(menu) {
      if (typeof menu == 'string' && menu[0] == '#') {
        menu = this.ownerDocument.getElementById(menu.slice(1));
        cr.ui.decorate(menu, Menu);
      }

      this.menu_ = menu;
      if (menu) {
        if (menu.id)
          this.setAttribute('menu', '#' + menu.id);
      }
    },

    /**
     * Handles event callbacks.
     * @param {Event} e The event object.
     */
    handleEvent: function(e) {
      if (!this.menu)
        return;

      switch (e.type) {
        case 'mousedown':
          if (e.currentTarget == this.ownerDocument) {
            if (!this.contains(e.target) && !this.menu.contains(e.target))
              this.hideMenu();
            else
              e.preventDefault();
          } else {
            if (this.isMenuShown()) {
              this.hideMenu();
            } else if (e.button == 0) {  // Only show the menu when using left
                                         // mouse button.
              this.showMenu();
              // Prevent the button from stealing focus on mousedown.
              e.preventDefault();
            }
          }
          break;
        case 'keydown':
          this.handleKeyDown(e);
          // If the menu is visible we let it handle all the keyboard events.
          if (this.isMenuShown() && e.currentTarget == this.ownerDocument) {
            this.menu.handleKeyDown(e);
            e.preventDefault();
            e.stopPropagation();
          }
          break;

        case 'activate':
        case 'blur':
        case 'resize':
          this.hideMenu();
          break;
      }
    },

    /**
     * Shows the menu.
     */
    showMenu: function() {
      this.hideMenu();

      this.menu.style.display = 'block';
      this.setAttribute('menu-shown', '');

      // when the menu is shown we steal all keyboard events.
      var doc = this.ownerDocument;
      var win = doc.defaultView;
      this.showingEvents_.add(doc, 'keydown', this, true);
      this.showingEvents_.add(doc, 'mousedown', this, true);
      this.showingEvents_.add(doc, 'blur', this, true);
      this.showingEvents_.add(win, 'resize', this);
      this.showingEvents_.add(this.menu, 'activate', this);
      this.positionMenu_();
    },

    /**
     * Hides the menu. If your menu can go out of scope, make sure to call this
     * first.
     */
    hideMenu: function() {
      if (!this.isMenuShown())
        return;

      this.removeAttribute('menu-shown');
      this.menu.style.display = 'none';

      this.showingEvents_.removeAll();
      this.menu.selectedIndex = -1;
    },

    /**
     * Whether the menu is shown.
     */
    isMenuShown: function() {
      return this.hasAttribute('menu-shown');
    },

    /**
     * Positions the menu below the menu button. At this point we do not use any
     * advanced positioning logic to ensure the menu fits in the viewport.
     * @private
     */
    positionMenu_: function() {
      positionPopupAroundElement(this, this.menu, this.anchorType,
                                 this.invertLeftRight);
    },

    /**
     * Handles the keydown event for the menu button.
     */
    handleKeyDown: function(e) {
      switch (e.keyIdentifier) {
        case 'Down':
        case 'Up':
        case 'Enter':
        case 'U+0020': // Space
          if (!this.isMenuShown())
            this.showMenu();
          e.preventDefault();
          break;
        case 'Esc':
        case 'U+001B': // Maybe this is remote desktop playing a prank?
          this.hideMenu();
          break;
      }
    }
  };

  // Export
  return {
    MenuButton: MenuButton
  };
});
</script>
<script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

cr.define('cr.ui', function() {

  const Menu = cr.ui.Menu;

  /**
   * Handles context menus.
   * @constructor
   */
  function ContextMenuHandler() {}

  ContextMenuHandler.prototype = {

    /**
     * The menu that we are currently showing.
     * @type {cr.ui.Menu}
     */
    menu_: null,
    get menu() {
      return this.menu_;
    },

    /**
     * Shows a menu as a context menu.
     * @param {!Event} e The event triggering the show (usally a contextmenu
     *     event).
     * @param {!cr.ui.Menu} menu The menu to show.
     */
    showMenu: function(e, menu) {
      this.menu_ = menu;

      menu.style.display = 'block';
      // when the menu is shown we steal all keyboard events.
      var doc = menu.ownerDocument;
      doc.addEventListener('keydown', this, true);
      doc.addEventListener('mousedown', this, true);
      doc.addEventListener('blur', this, true);
      doc.defaultView.addEventListener('resize', this);
      menu.addEventListener('contextmenu', this);
      menu.addEventListener('activate', this);
      this.positionMenu_(e, menu);
    },

    /**
     * Hide the currently shown menu.
     */
    hideMenu: function() {
      var menu = this.menu;
      if (!menu)
        return;

      menu.style.display = 'none';
      var doc = menu.ownerDocument;
      doc.removeEventListener('keydown', this, true);
      doc.removeEventListener('mousedown', this, true);
      doc.removeEventListener('blur', this, true);
      doc.defaultView.removeEventListener('resize', this);
      menu.removeEventListener('contextmenu', this);
      menu.removeEventListener('activate', this);
      menu.selectedIndex = -1;
      this.menu_ = null;

      // On windows we might hide the menu in a right mouse button up and if
      // that is the case we wait some short period before we allow the menu
      // to be shown again.
      this.hideTimestamp_ = cr.isWindows ? Date.now() : 0;
    },

    /**
     * Positions the menu
     * @param {!Event} e The event object triggering the showing.
     * @param {!cr.ui.Menu} menu The menu to position.
     * @private
     */
    positionMenu_: function(e, menu) {
      // TODO(arv): Handle scrolled documents when needed.

      var element = e.currentTarget;
      var x, y;
      // When the user presses the context menu key (on the keyboard) we need
      // to detect this.
      if (this.keyIsDown_) {
        var rect = element.getRectForContextMenu ?
                       element.getRectForContextMenu() :
                       element.getBoundingClientRect();
        var offset = Math.min(rect.width, rect.height) / 2;
        x = rect.left + offset;
        y = rect.top + offset;
      } else {
        x = e.clientX;
        y = e.clientY;
      }

      cr.ui.positionPopupAtPoint(x, y, menu);
    },

    /**
     * Handles event callbacks.
     * @param {!Event} e The event object.
     */
    handleEvent: function(e) {
      // Keep track of keydown state so that we can use that to determine the
      // reason for the contextmenu event.
      switch (e.type) {
        case 'keydown':
          this.keyIsDown_ = !e.ctrlKey && !e.altKey &&
              // context menu key or Shift-F10
              (e.keyCode == 93 && !e.shiftKey ||
               e.keyIdentifier == 'F10' && e.shiftKey);
          break;

        case 'keyup':
          this.keyIsDown_ = false;
          break;
      }

      // Context menu is handled even when we have no menu.
      if (e.type != 'contextmenu' && !this.menu)
        return;

      switch (e.type) {
        case 'mousedown':
          if (!this.menu.contains(e.target))
            this.hideMenu();
          else
            e.preventDefault();
          break;
        case 'keydown':
          // keyIdentifier does not report 'Esc' correctly
          if (e.keyCode == 27 /* Esc */) {
            this.hideMenu();

          // If the menu is visible we let it handle all the keyboard events.
          } else if (this.menu) {
            this.menu.handleKeyDown(e);
            e.preventDefault();
            e.stopPropagation();
          }
          break;

        case 'activate':
        case 'blur':
        case 'resize':
          this.hideMenu();
          break;

        case 'contextmenu':
          if ((!this.menu || !this.menu.contains(e.target)) &&
              (!this.hideTimestamp_ || Date.now() - this.hideTimestamp_ > 50))
            this.showMenu(e, e.currentTarget.contextMenu);
          e.preventDefault();
          // Don't allow elements further up in the DOM to show their menus.
          e.stopPropagation();
          break;
      }
    },

    /**
     * Adds a contextMenu property to an element or element class.
     * @param {!Element|!Function} element The element or class to add the
     *     contextMenu property to.
     */
    addContextMenuProperty: function(element) {
      if (typeof element == 'function')
        element = element.prototype;

      element.__defineGetter__('contextMenu', function() {
        return this.contextMenu_;
      });
      element.__defineSetter__('contextMenu', function(menu) {
        var oldContextMenu = this.contextMenu;

        if (typeof menu == 'string' && menu[0] == '#') {
          menu = this.ownerDocument.getElementById(menu.slice(1));
          cr.ui.decorate(menu, Menu);
        }

        if (menu === oldContextMenu)
          return;

        if (oldContextMenu && !menu) {
          this.removeEventListener('contextmenu', contextMenuHandler);
          this.removeEventListener('keydown', contextMenuHandler);
          this.removeEventListener('keyup', contextMenuHandler);
        }
        if (menu && !oldContextMenu) {
          this.addEventListener('contextmenu', contextMenuHandler);
          this.addEventListener('keydown', contextMenuHandler);
          this.addEventListener('keyup', contextMenuHandler);
        }

        this.contextMenu_ = menu;

        if (menu && menu.id)
          this.setAttribute('contextmenu', '#' + menu.id);

        cr.dispatchPropertyChange(this, 'contextMenu', menu, oldContextMenu);
      });

      if (!element.getRectForContextMenu) {
        /**
         * @return {!ClientRect} The rect to use for positioning the context
         *     menu when the context menu is not opened using a mouse position.
         */
        element.getRectForContextMenu = function() {
          return this.getBoundingClientRect();
        };
      }
    }
  };

  /**
   * The singleton context menu handler.
   * @type {!ContextMenuHandler}
   */
  var contextMenuHandler = new ContextMenuHandler;

  // Export
  return {
    contextMenuHandler: contextMenuHandler
  };
});
</script>

<script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview DragWrapper
 * A class for simplifying HTML5 drag and drop. Classes should use this to
 * handle the nitty gritty of nested drag enters and leaves.
 */
var DragWrapper = (function() {
  /**
   * Creates a DragWrapper which listens for drag target events on |target| and
   * delegates event handling to |handler|. The |handler| must implement:
   *   shouldAcceptDrag
   *   doDragEnter
   *   doDragLeave
   *   doDragOver
   *   doDrop
   */
  function DragWrapper(target, handler) {
    this.initialize(target, handler);
  }

  DragWrapper.prototype = {
    initialize: function(target, handler) {
      target.addEventListener('dragenter',
                              this.onDragEnter_.bind(this));
      target.addEventListener('dragover', this.onDragOver_.bind(this));
      target.addEventListener('drop', this.onDrop_.bind(this));
      target.addEventListener('dragleave', this.onDragLeave_.bind(this));

      this.target_ = target;
      this.handler_ = handler;
    },

    /**
     * The number of un-paired dragenter events that have fired on |this|. This
     * is incremented by |onDragEnter_| and decremented by |onDragLeave_|. This
     * is necessary because dragging over child widgets will fire additional
     * enter and leave events on |this|. A non-zero value does not necessarily
     * indicate that |isCurrentDragTarget()| is true.
     * @type {number}
     * @private
     */
    dragEnters_: 0,

    /**
     * Whether the tile page is currently being dragged over with data it can
     * accept.
     * @type {boolean}
     */
    get isCurrentDragTarget() {
      return this.target_.classList.contains('drag-target');
    },

    /**
     * Handler for dragenter events fired on |target_|.
     * @param {Event} e A MouseEvent for the drag.
     * @private
     */
    onDragEnter_: function(e) {
      if (++this.dragEnters_ == 1) {
        if (this.handler_.shouldAcceptDrag(e)) {
          this.target_.classList.add('drag-target');
          this.handler_.doDragEnter(e);
        }
      } else {
        // Sometimes we'll get an enter event over a child element without an
        // over event following it. In this case we have to still call the
        // drag over handler so that we make the necessary updates (one visible
        // symptom of not doing this is that the cursor's drag state will
        // flicker during drags).
        this.onDragOver_(e);
      }
    },

    /**
     * Thunk for dragover events fired on |target_|.
     * @param {Event} e A MouseEvent for the drag.
     * @private
     */
    onDragOver_: function(e) {
      if (!this.target_.classList.contains('drag-target'))
        return;
      this.handler_.doDragOver(e);
    },

    /**
     * Thunk for drop events fired on |target_|.
     * @param {Event} e A MouseEvent for the drag.
     * @private
     */
    onDrop_: function(e) {
      this.dragEnters_ = 0;
      if (!this.target_.classList.contains('drag-target'))
        return;
      this.target_.classList.remove('drag-target');
      this.handler_.doDrop(e);
    },

    /**
     * Thunk for dragleave events fired on |target_|.
     * @param {Event} e A MouseEvent for the drag.
     * @private
     */
    onDragLeave_: function(e) {
      if (--this.dragEnters_ > 0)
        return;

      this.target_.classList.remove('drag-target');
      this.handler_.doDragLeave(e);
    },
  };

  return DragWrapper;
})();
</script>
<script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

cr.define('ntp4', function() {
  'use strict';

  // We can't pass the currently dragging tile via dataTransfer because of
  // http://crbug.com/31037
  var currentlyDraggingTile = null;
  function getCurrentlyDraggingTile() {
    return currentlyDraggingTile;
  }
  function setCurrentlyDraggingTile(tile) {
    currentlyDraggingTile = tile;
    if (tile)
      ntp4.enterRearrangeMode();
    else
      ntp4.leaveRearrangeMode();
  }

  /**
   * Creates a new Tile object. Tiles wrap content on a TilePage, providing
   * some styling and drag functionality.
   * @constructor
   * @extends {HTMLDivElement}
   */
  function Tile(contents) {
    var tile = cr.doc.createElement('div');
    tile.__proto__ = Tile.prototype;
    tile.initialize(contents);

    return tile;
  }

  Tile.prototype = {
    __proto__: HTMLDivElement.prototype,

    initialize: function(contents) {
      // 'real' as opposed to doppleganger.
      this.className = 'tile real';
      this.appendChild(contents);
      contents.tile = this;

      this.addEventListener('dragstart', this.onDragStart_);
      this.addEventListener('drag', this.onDragMove_);
      this.addEventListener('dragend', this.onDragEnd_);

      this.firstChild.addEventListener(
          'webkitAnimationEnd', this.onContentsAnimationEnd_.bind(this));

      this.eventTracker = new EventTracker();
    },

    get index() {
      return Array.prototype.indexOf.call(this.parentNode.children, this);
    },

    get tilePage() {
      return findAncestorByClass(this, 'tile-page');
    },

    /**
     * Position the tile at |x, y|, and store this as the grid location, i.e.
     * where the tile 'belongs' when it's not being dragged.
     * @param {number} x The x coordinate, in pixels.
     * @param {number} y The y coordinate, in pixels.
     */
    setGridPosition: function(x, y) {
      this.gridX = x;
      this.gridY = y;
      this.moveTo(x, y);
    },

    /**
     * Position the tile at |x, y|.
     * @param {number} x The x coordinate, in pixels.
     * @param {number} y The y coordinate, in pixels.
     */
    moveTo: function(x, y) {
      // left overrides right in LTR, and right takes precedence in RTL.
      this.style.left = x + 'px';
      this.style.right = x + 'px';
      this.style.top = y + 'px';
    },

    /**
     * The handler for dragstart events fired on |this|.
     * @param {Event} e The event for the drag.
     * @private
     */
    onDragStart_: function(e) {
      // The user may start dragging again during a previous drag's finishing
      // animation.
      if (this.classList.contains('dragging'))
        this.finalizeDrag_();

      setCurrentlyDraggingTile(this);

      e.dataTransfer.effectAllowed = 'copyMove';
      this.firstChild.setDragData(e.dataTransfer);

      // The drag clone is the node we use as a representation during the drag.
      // It's attached to the top level document element so that it floats above
      // image masks.
      this.dragClone = this.cloneNode(true);
      this.dragClone.style.right = '';
      this.dragClone.classList.add('drag-representation');
      $('card-slider-frame').appendChild(this.dragClone);
      this.eventTracker.add(this.dragClone, 'webkitTransitionEnd',
                            this.onDragCloneTransitionEnd_.bind(this));

      this.classList.add('dragging');
      // offsetLeft is mirrored in RTL. Un-mirror it.
      var offsetLeft = ntp4.isRTL() ?
          this.parentNode.clientWidth - this.offsetLeft :
          this.offsetLeft;
      this.dragOffsetX = e.x - offsetLeft - this.parentNode.offsetLeft;
      this.dragOffsetY = e.y - this.offsetTop -
          // Unlike offsetTop, this value takes scroll position into account.
          this.parentNode.getBoundingClientRect().top;

      this.onDragMove_(e);
    },

    /**
     * The handler for drag events fired on |this|.
     * @param {Event} e The event for the drag.
     * @private
     */
    onDragMove_: function(e) {
      if (e.view != window || (e.x == 0 && e.y == 0)) {
        this.dragClone.hidden = true;
        return;
      }

      this.dragClone.hidden = false;
      this.dragClone.style.left = (e.x - this.dragOffsetX) + 'px';
      this.dragClone.style.top = (e.y - this.dragOffsetY) + 'px';
    },

    /**
     * The handler for dragend events fired on |this|.
     * @param {Event} e The event for the drag.
     * @private
     */
    onDragEnd_: function(e) {
      this.dragClone.hidden = false;
      this.dragClone.classList.add('placing');

      setCurrentlyDraggingTile(null);

      // tilePage will be null if we've already been removed.
      var tilePage = this.tilePage;
      if (tilePage)
        tilePage.positionTile_(this.index);

      // Take an appropriate action with the drag clone.
      if (this.landedOnTrash) {
        this.dragClone.classList.add('deleting');
      } else if (tilePage) {
        // TODO(dbeam): Until we fix dropEffect to the correct behavior it will
        // differ on windows - crbug.com/39399.  That's why we use the custom
        // tilePage.lastDropEffect_ instead of e.dataTransfer.dropEffect.
        if (tilePage.selected && tilePage.lastDropEffect_ != 'copy') {
          // The drag clone can still be hidden from the last drag move event.
          this.dragClone.hidden = false;
          // The tile's contents may have moved following the respositioning;
          // adjust for that.
          var contentDiffX = this.dragClone.firstChild.offsetLeft -
              this.firstChild.offsetLeft;
          var contentDiffY = this.dragClone.firstChild.offsetTop -
              this.firstChild.offsetTop;
          this.dragClone.style.left = (this.gridX + this.parentNode.offsetLeft -
              contentDiffX) + 'px';
          this.dragClone.style.top =
              (this.gridY + this.parentNode.getBoundingClientRect().top -
              contentDiffY) + 'px';
        } else if (this.dragClone.hidden) {
          this.finalizeDrag_();
        } else {
          // The CSS3 transitions spec intentionally leaves it up to individual
          // user agents to determine when styles should be applied. On some
          // platforms (at the moment, Windows), when you apply both classes
          // immediately a transition may not occur correctly. That's why we're
          // using a setTimeout here to queue adding the class until the
          // previous class (currently: .placing) sets up a transition.
          // http://dev.w3.org/csswg/css3-transitions/#starting
          window.setTimeout(function() {
            this.dragClone.classList.add('dropped-on-other-page');
          }.bind(this), 0);
        }
      }

      this.landedOnTrash = false;
    },

    /**
     * Creates a clone of this node offset by the coordinates. Used for the
     * dragging effect where a tile appears to float off one side of the grid
     * and re-appear on the other.
     * @param {number} x x-axis offset, in pixels.
     * @param {number} y y-axis offset, in pixels.
     */
    showDoppleganger: function(x, y) {
      // We always have to clear the previous doppleganger to make sure we get
      // style updates for the contents of this tile.
      this.clearDoppleganger();

      var clone = this.cloneNode(true);
      clone.classList.remove('real');
      clone.classList.add('doppleganger');
      var clonelets = clone.querySelectorAll('.real');
      for (var i = 0; i < clonelets.length; i++) {
        clonelets[i].classList.remove('real');
      }

      this.appendChild(clone);
      this.doppleganger_ = clone;

      if (ntp4.isRTL())
        x *= -1;

      this.doppleganger_.style.WebkitTransform = 'translate(' + x + 'px, ' +
                                                                y + 'px)';
    },

    /**
     * Destroys the current doppleganger.
     */
    clearDoppleganger: function() {
      if (this.doppleganger_) {
        this.removeChild(this.doppleganger_);
        this.doppleganger_ = null;
      }
    },

    /**
     * Returns status of doppleganger.
     * @return {boolean} True if there is a doppleganger showing for |this|.
     */
    hasDoppleganger: function() {
      return !!this.doppleganger_;
    },

    /**
     * Cleans up after the drag is over. This is either called when the
     * drag representation finishes animating to the final position, or when
     * the next drag starts (if the user starts a 2nd drag very quickly).
     * @private
     */
    finalizeDrag_: function() {
      assert(this.classList.contains('dragging'));

      var clone = this.dragClone;
      this.dragClone = null;

      clone.parentNode.removeChild(clone);
      this.eventTracker.remove(clone, 'webkitTransitionEnd');
      this.classList.remove('dragging');
      if (this.firstChild.finalizeDrag)
        this.firstChild.finalizeDrag();
    },

    /**
     * Called when the drag representation node is done migrating to its final
     * resting spot.
     * @param {Event} e The transition end event.
     */
    onDragCloneTransitionEnd_: function(e) {
      if (this.classList.contains('dragging') &&
          (e.propertyName == 'left' || e.propertyName == 'top' ||
           e.propertyName == '-webkit-transform')) {
        this.finalizeDrag_();
      }
    },

    /**
     * Called when an app is removed from Chrome. Animates its disappearance.
     */
    doRemove: function() {
      this.firstChild.classList.add('removing-tile-contents');
    },

    /**
     * Callback for the webkitAnimationEnd event on the tile's contents.
     * @param {Event} e The event object.
     */
    onContentsAnimationEnd_: function(e) {
      if (this.firstChild.classList.contains('new-tile-contents'))
        this.firstChild.classList.remove('new-tile-contents');
      if (this.firstChild.classList.contains('removing-tile-contents'))
        this.tilePage.removeTile(this);
    },
  };

  /**
   * Gives the proportion of the row width that is devoted to a single icon.
   * @param {number} rowTileCount The number of tiles in a row.
   * @param {number} tileSpacingFraction The proportion of the tile width which
   *     will be used as spacing between tiles.
   * @return {number} The ratio between icon width and row width.
   */
  function tileWidthFraction(rowTileCount, tileSpacingFraction) {
    return rowTileCount + (rowTileCount - 1) * tileSpacingFraction;
  }

  /**
   * Calculates an assortment of tile-related values for a grid with the
   * given dimensions.
   * @param {number} width The pixel width of the grid.
   * @param {number} numRowTiles The number of tiles in a row.
   * @param {number} tileSpacingFraction The proportion of the tile width which
   *     will be used as spacing between tiles.
   * @return {Object} A mapping of pixel values.
   */
  function tileValuesForGrid(width, numRowTiles, tileSpacingFraction) {
    var tileWidth = width / tileWidthFraction(numRowTiles, tileSpacingFraction);
    var offsetX = tileWidth * (1 + tileSpacingFraction);
    var interTileSpacing = offsetX - tileWidth;

    return {
      tileWidth: tileWidth,
      offsetX: offsetX,
      interTileSpacing: interTileSpacing,
    };
  }

  // The smallest amount of horizontal blank space to display on the sides when
  // displaying a wide arrangement. There is an additional 26px of margin from
  // the tile page padding.
  var MIN_WIDE_MARGIN = 18;

  /**
   * Creates a new TilePage object. This object contains tiles and controls
   * their layout.
   * @param {Object} gridValues Pixel values that define the size and layout
   *     of the tile grid.
   * @constructor
   * @extends {HTMLDivElement}
   */
  function TilePage(gridValues) {
    var el = cr.doc.createElement('div');
    el.gridValues_ = gridValues;
    el.__proto__ = TilePage.prototype;
    el.initialize();

    return el;
  }

  /**
   * Takes a collection of grid layout pixel values and updates them with
   * additional tiling values that are calculated from TilePage constants.
   * @param {Object} grid The grid layout pixel values to update.
   */
  TilePage.initGridValues = function(grid) {
    // The amount of space we need to display a narrow grid (all narrow grids
    // are this size).
    grid.narrowWidth =
        grid.minTileWidth * tileWidthFraction(grid.minColCount,
                                              grid.tileSpacingFraction);
    // The minimum amount of space we need to display a wide grid.
    grid.minWideWidth =
        grid.minTileWidth * tileWidthFraction(grid.maxColCount,
                                              grid.tileSpacingFraction);
    // The largest we will ever display a wide grid.
    grid.maxWideWidth =
        grid.maxTileWidth * tileWidthFraction(grid.maxColCount,
                                              grid.tileSpacingFraction);
    // Tile-related pixel values for the narrow display.
    grid.narrowTileValues = tileValuesForGrid(grid.narrowWidth,
                                              grid.minColCount,
                                              grid.tileSpacingFraction);
    // Tile-related pixel values for the minimum narrow display.
    grid.wideTileValues = tileValuesForGrid(grid.minWideWidth,
                                            grid.maxColCount,
                                            grid.tileSpacingFraction);
  };

  TilePage.prototype = {
    __proto__: HTMLDivElement.prototype,

    initialize: function() {
      this.className = 'tile-page';

      // Div that acts as a custom scrollbar. The scrollbar has to live
      // outside the content div so it doesn't flicker when scrolling (due to
      // repainting after the scroll, then repainting again when moved in the
      // onScroll handler). |scrollbar_| is only aesthetic, and it only
      // represents the thumb. Actual events are still handled by the invisible
      // native scrollbars. This div gives us more flexibility with the visuals.
      this.scrollbar_ = this.ownerDocument.createElement('div');
      this.scrollbar_.className = 'tile-page-scrollbar';
      this.scrollbar_.hidden = true;
      this.appendChild(this.scrollbar_);

      // This contains everything but the scrollbar.
      this.content_ = this.ownerDocument.createElement('div');
      this.content_.className = 'tile-page-content';
      this.appendChild(this.content_);

      // Div that sets the vertical position of the tile grid.
      this.topMargin_ = this.ownerDocument.createElement('div');
      this.topMargin_.className = 'top-margin';
      this.content_.appendChild(this.topMargin_);

      // Div that holds the tiles.
      this.tileGrid_ = this.ownerDocument.createElement('div');
      this.tileGrid_.className = 'tile-grid';
      this.tileGrid_.style.minWidth = this.gridValues_.narrowWidth + 'px';
      this.content_.appendChild(this.tileGrid_);

      // Ordered list of our tiles.
      this.tileElements_ = this.tileGrid_.getElementsByClassName('tile real');

      // These are properties used in updateTopMargin.
      this.animatedTopMarginPx_ = 0;
      this.topMarginPx_ = 0;

      this.eventTracker = new EventTracker();
      this.eventTracker.add(window, 'resize', this.onResize_.bind(this));

      this.addEventListener('DOMNodeInsertedIntoDocument',
                            this.onNodeInsertedIntoDocument_);

      this.addEventListener('mousewheel', this.onMouseWheel_);
      this.content_.addEventListener('scroll', this.onScroll_.bind(this));

      this.dragWrapper_ = new DragWrapper(this.tileGrid_, this);

      $('page-list').addEventListener(
          CardSlider.EventType.CARD_CHANGED,
          this.onCardChanged.bind(this));
    },

    get tiles() {
      return this.tileElements_;
    },

    get tileCount() {
      return this.tileElements_.length;
    },

    get selected() {
      return Array.prototype.indexOf.call(this.parentNode.children, this) ==
          ntp4.getCardSlider().currentCard;
    },

    /**
     * The size of the margin (unused space) on the sides of the tile grid, in
     * pixels.
     * @type {number}
     */
    get sideMargin() {
      return this.layoutValues_.leftMargin;
    },

    /**
     * Returns the width of the scrollbar, in pixels, if it is active, or 0
     * otherwise.
     * @type {number}
     */
    get scrollbarWidth() {
      return this.scrollbar_.hidden ? 0 : 13;
    },

    /**
     * Returns any extra padding to insert to the bottom of a tile page.  By
     * default there is none, but subclasses can override.
     * @type {number}
     */
    get extraBottomPadding() {
      return 0;
    },

    /**
     * Cleans up resources that are no longer needed after this TilePage
     * instance is removed from the DOM.
     */
    tearDown: function() {
      this.eventTracker.removeAll();
    },

    /**
     * Appends a tile to the end of the tile grid.
     * @param {HTMLElement} tileElement The contents of the tile.
     * @param {?boolean} animate If true, the append will be animated.
     * @protected
     */
    appendTile: function(tileElement, animate) {
      this.addTileAt(tileElement, this.tileElements_.length, animate);
    },

    /**
     * Adds the given element to the tile grid.
     * @param {Node} tileElement The tile object/node to insert.
     * @param {number} index The location in the tile grid to insert it at.
     * @param {?boolean} animate If true, the tile in question will be animated
     *     (other tiles, if they must reposition, do not animate).
     * @protected
     */
    addTileAt: function(tileElement, index, animate) {
      this.classList.remove('animating-tile-page');
      if (animate)
        tileElement.classList.add('new-tile-contents');
      var wrapperDiv = new Tile(tileElement);
      if (index == this.tileElements_.length) {
        this.tileGrid_.appendChild(wrapperDiv);
      } else {
        this.tileGrid_.insertBefore(wrapperDiv,
                                    this.tileElements_[index]);
      }
      this.calculateLayoutValues_();
      this.heightChanged_();

      this.positionTile_(index);
    },

    /**
     * Removes the given tile and animates the respositioning of the other
     * tiles.
     * @param {HTMLElement} tile The tile to remove from |tileGrid_|.
     * @param {?boolean} animate If true, tiles will animate.
     */
    removeTile: function(tile, animate) {
      if (animate)
        this.classList.add('animating-tile-page');
      var index = tile.index;
      tile.parentNode.removeChild(tile);
      this.calculateLayoutValues_();
      for (var i = index; i < this.tileElements_.length; i++) {
        this.positionTile_(i);
      }
    },

    /**
     * Removes all tiles from the page.
     */
    removeAllTiles: function() {
      this.tileGrid_.innerHTML = '';
    },

    /**
     * Makes some calculations for tile layout. These change depending on
     * height, width, and the number of tiles.
     * TODO(estade): optimize calls to this function. Do nothing if the page is
     * hidden, but call before being shown.
     * @private
     */
    calculateLayoutValues_: function() {
      var grid = this.gridValues_;
      var availableSpace = this.tileGrid_.clientWidth - 2 * MIN_WIDE_MARGIN;
      var wide = availableSpace >= grid.minWideWidth;
      var numRowTiles = wide ? grid.maxColCount : grid.minColCount;

      var effectiveGridWidth = wide ?
          Math.min(Math.max(availableSpace, grid.minWideWidth),
                   grid.maxWideWidth) :
          grid.narrowWidth;
      var realTileValues = tileValuesForGrid(effectiveGridWidth, numRowTiles,
                                             grid.tileSpacingFraction);

      // leftMargin centers the grid within the avaiable space.
      var minMargin = wide ? MIN_WIDE_MARGIN : 0;
      var leftMargin =
          Math.max(minMargin,
                   (this.tileGrid_.clientWidth - effectiveGridWidth) / 2);

      var rowHeight = this.heightForWidth(realTileValues.tileWidth) +
          realTileValues.interTileSpacing;

      this.layoutValues_ = {
        numRowTiles: numRowTiles,
        leftMargin: leftMargin,
        colWidth: realTileValues.offsetX,
        rowHeight: rowHeight,
        tileWidth: realTileValues.tileWidth,
        wide: wide,
      };

      // We need to update the top margin as well.
      this.updateTopMargin_();

      this.firePageLayoutEvent_();
    },

    /**
     * Dispatches the custom pagelayout event.
     * @private
     */
    firePageLayoutEvent_: function() {
      cr.dispatchSimpleEvent(this, 'pagelayout', true, true);
    },

    /**
     * Calculates the x/y coordinates for an element and moves it there.
     * @param {number} index The index of the element to be positioned.
     * @param {number} indexOffset If provided, this is added to |index| when
     *     positioning the tile. The effect is that the tile will be positioned
     *     in a non-default location.
     * @private
     */
    positionTile_: function(index, indexOffset) {
      var grid = this.gridValues_;
      var layout = this.layoutValues_;

      indexOffset = typeof indexOffset != 'undefined' ? indexOffset : 0;
      // Add the offset _after_ the modulus division. We might want to show the
      // tile off the side of the grid.
      var col = index % layout.numRowTiles + indexOffset;
      var row = Math.floor(index / layout.numRowTiles);
      // Calculate the final on-screen position for the tile.
      var realX = col * layout.colWidth + layout.leftMargin;
      var realY = row * layout.rowHeight;

      // Calculate the portion of the tile's position that should be animated.
      var animatedTileValues = layout.wide ?
          grid.wideTileValues : grid.narrowTileValues;
      // Animate the difference between three-wide and six-wide.
      var animatedLeftMargin = layout.wide ?
          0 : (grid.minWideWidth - MIN_WIDE_MARGIN - grid.narrowWidth) / 2;
      var animatedX = col * animatedTileValues.offsetX + animatedLeftMargin;
      var animatedY = row * (this.heightForWidth(animatedTileValues.tileWidth) +
                             animatedTileValues.interTileSpacing);

      var tile = this.tileElements_[index];
      tile.setGridPosition(animatedX, animatedY);
      tile.firstChild.setBounds(layout.tileWidth,
                                realX - animatedX,
                                realY - animatedY);

      // This code calculates whether the tile needs to show a clone of itself
      // wrapped around the other side of the tile grid.
      var offTheRight = col == layout.numRowTiles ||
          (col == layout.numRowTiles - 1 && tile.hasDoppleganger());
      var offTheLeft = col == -1 || (col == 0 && tile.hasDoppleganger());
      if (this.isCurrentDragTarget && (offTheRight || offTheLeft)) {
        var sign = offTheRight ? 1 : -1;
        tile.showDoppleganger(-layout.numRowTiles * layout.colWidth * sign,
                              layout.rowHeight * sign);
      } else {
        tile.clearDoppleganger();
      }

      if (index == this.tileElements_.length - 1) {
        this.tileGrid_.style.height = (realY + layout.rowHeight) + 'px';
        this.queueUpdateScrollbars_();
      }
    },

    /**
     * Gets the index of the tile that should occupy coordinate (x, y). Note
     * that this function doesn't care where the tiles actually are, and will
     * return an index even for the space between two tiles. This function is
     * effectively the inverse of |positionTile_|.
     * @param {number} x The x coordinate, in pixels, relative to the left of
     *     |this|.
     * @param {number} y The y coordinate, in pixels, relative to the top of
     *     |this|.
     * @private
     */
    getWouldBeIndexForPoint_: function(x, y) {
      var grid = this.gridValues_;
      var layout = this.layoutValues_;

      var gridClientRect = this.tileGrid_.getBoundingClientRect();
      var col = Math.floor((x - gridClientRect.left - layout.leftMargin) /
                           layout.colWidth);
      if (col < 0 || col >= layout.numRowTiles)
        return -1;

      if (ntp4.isRTL())
        col = layout.numRowTiles - 1 - col;

      var row = Math.floor((y - gridClientRect.top) / layout.rowHeight);
      return row * layout.numRowTiles + col;
    },

    /**
     * Window resize event handler. Window resizes may trigger re-layouts.
     * @param {Object} e The resize event.
     */
    onResize_: function(e) {
      if (this.lastWidth_ == this.clientWidth &&
          this.lastHeight_ == this.clientHeight) {
        return;
      }

      this.calculateLayoutValues_();

      this.lastWidth_ = this.clientWidth;
      this.lastHeight_ = this.clientHeight;
      this.classList.add('animating-tile-page');
      this.heightChanged_();

      this.repositionTiles_();
    },

    /**
     * The tile grid has an image mask which fades at the edges. We only show
     * the mask when there is an active drag; it obscures doppleganger tiles
     * as they enter or exit the grid.
     * @private
     */
    updateMask_: function() {
      if (!this.isCurrentDragTarget) {
        this.tileGrid_.style.WebkitMaskBoxImage = '';
        return;
      }

      var leftMargin = this.layoutValues_.leftMargin;
      var fadeDistance = Math.min(leftMargin, 20);
      var gradient =
          '-webkit-linear-gradient(left,' +
              'transparent, ' +
              'transparent ' + (leftMargin - fadeDistance) + 'px, ' +
              'black ' + leftMargin + 'px, ' +
              'black ' + (this.tileGrid_.clientWidth - leftMargin) + 'px, ' +
              'transparent ' + (this.tileGrid_.clientWidth - leftMargin +
                                fadeDistance) + 'px, ' +
              'transparent)';
      this.tileGrid_.style.WebkitMaskBoxImage = gradient;
    },

    updateTopMargin_: function() {
      var layout = this.layoutValues_;

      // The top margin is set so that the vertical midpoint of the grid will
      // be 1/3 down the page.
      var numTiles = this.tileCount +
          (this.isCurrentDragTarget && !this.withinPageDrag_ ? 1 : 0);
      var numRows = Math.ceil(numTiles / layout.numRowTiles);
      var usedHeight = layout.rowHeight * numRows;
      // 100 matches the top padding of tile-page.
      var newMargin = document.documentElement.clientHeight / 3 -
          usedHeight / 2 - 100 - this.content_.offsetTop;
      newMargin = Math.max(newMargin, 0);

      // |newMargin| is the final margin we actually want to show. However,
      // part of that should be animated and part should not (for the same
      // reason as with leftMargin). The approach is to consider differences
      // when the layout changes from wide to narrow or vice versa as
      // 'animatable'. These differences accumulate in animatedTopMarginPx_,
      // while topMarginPx_ caches the real (total) margin. Either of these
      // calculations may come out to be negative, so we use margins as the
      // css property.

      if (typeof this.topMarginIsForWide_ == 'undefined')
        this.topMarginIsForWide_ = layout.wide;
      if (this.topMarginIsForWide_ != layout.wide) {
        this.animatedTopMarginPx_ += newMargin - this.topMarginPx_;
        this.topMargin_.style.marginBottom =
            this.animatedTopMarginPx_ + 'px';
      }

      this.topMarginIsForWide_ = layout.wide;
      this.topMarginPx_ = newMargin;
      this.topMargin_.style.marginTop =
          (this.topMarginPx_ - this.animatedTopMarginPx_) + 'px';
    },

    /**
     * Handles final setup that can only happen after |this| is inserted into
     * the page.
     * @private
     */
    onNodeInsertedIntoDocument_: function(e) {
      this.calculateLayoutValues_();
      this.heightChanged_();
    },

    /**
     * Called when the height of |this| has changed: update the size of
     * tileGrid.
     * @private
     */
    heightChanged_: function() {
      // The tile grid will expand to the bottom footer, or enough to hold all
      // the tiles, whichever is greater. It would be nicer if tilePage were
      // a flex box, and the tile grid could be box-flex: 1, but this exposes a
      // bug where repositioning tiles will cause the scroll position to reset.
      this.tileGrid_.style.minHeight = (this.clientHeight -
          this.tileGrid_.offsetTop - this.content_.offsetTop -
          this.extraBottomPadding) + 'px';
    },

    /**
     * Scrolls the page in response to a mousewheel event.
     * @param {Event} e The mousewheel event.
     */
    handleMouseWheel: function(e) {
      this.content_.scrollTop -= e.wheelDeltaY / 3;
    },

    /**
     * Handles mouse wheels on |this|. We handle this explicitly because we want
     * a consistent experience whether the user scrolls on the page or on the
     * page switcher (handleMouseWheel provides a common conversion factor
     * between wheel delta and scroll delta).
     * @param {Event} e The mousewheel event.
     * @private
     */
    onMouseWheel_: function(e) {
      if (e.wheelDeltaY == 0)
        return;

      this.handleMouseWheel(e);
      e.preventDefault();
      e.stopPropagation();
    },

    /**
     * Handler for the 'scroll' event on |content_|.
     * @param {Event} e The scroll event.
     * @private
     */
    onScroll_: function(e) {
      this.queueUpdateScrollbars_();
    },

    /**
     * ID of scrollbar update timer. If 0, there's no scrollbar re-calc queued.
     * @private
     */
    scrollbarUpdate_: 0,

    /**
     * Queues an update on the custom scrollbar. Used for two reasons: first,
     * coalescing of multiple updates, and second, because action like
     * repositioning a tile can require a delay before they affect values
     * like clientHeight.
     * @private
     */
    queueUpdateScrollbars_: function() {
      if (this.scrollbarUpdate_)
        return;

      this.scrollbarUpdate_ = window.setTimeout(
          this.doUpdateScrollbars_.bind(this), 0);
    },

    /**
     * Does the work of calculating the visibility, height and position of the
     * scrollbar thumb (there is no track or buttons).
     * @private
     */
    doUpdateScrollbars_: function() {
      this.scrollbarUpdate_ = 0;

      var content = this.content_;

      // Adjust scroll-height to account for possible header-bar.
      var adjustedScrollHeight = content.scrollHeight - content.offsetTop;

      if (adjustedScrollHeight <= content.clientHeight) {
        this.scrollbar_.hidden = true;
        return;
      } else {
        this.scrollbar_.hidden = false;
      }

      var thumbTop = content.offsetTop +
          content.scrollTop / adjustedScrollHeight * content.clientHeight;
      var thumbHeight = content.clientHeight / adjustedScrollHeight *
          this.clientHeight;

      this.scrollbar_.style.top = thumbTop + 'px';
      this.scrollbar_.style.height = thumbHeight + 'px';
      this.firePageLayoutEvent_();
    },

    /**
     * Get the height for a tile of a certain width. Override this function to
     * get non-square tiles.
     * @param {number} width The pixel width of a tile.
     * @return {number} The height for |width|.
     */
    heightForWidth: function(width) {
      return width;
    },

    /**
     * Handle for CARD_CHANGED.
     * @param {Event} e The card slider event.
     */
    onCardChanged: function(e) {
      // When we are selected, we re-calculate the layout values. (See comment
      // in doDrop.)
      if (e.cardSlider.currentCardValue == this)
        this.calculateLayoutValues_();
    },

    /** Dragging **/

    get isCurrentDragTarget() {
      return this.dragWrapper_.isCurrentDragTarget;
    },

    /**
     * Thunk for dragleave events fired on |tileGrid_|.
     * @param {Event} e A MouseEvent for the drag.
     */
    doDragLeave: function(e) {
      this.cleanupDrag();
    },

    /**
     * Performs all actions necessary when a drag enters the tile page.
     * @param {Event} e A mouseover event for the drag enter.
     */
    doDragEnter: function(e) {

      // Applies the mask so doppleganger tiles disappear into the fog.
      this.updateMask_();

      this.classList.add('animating-tile-page');
      this.withinPageDrag_ = this.contains(currentlyDraggingTile);
      this.dragItemIndex_ = this.withinPageDrag_ ?
          currentlyDraggingTile.index : this.tileElements_.length;
      this.currentDropIndex_ = this.dragItemIndex_;

      // The new tile may change the number of rows, hence the top margin
      // will change.
      if (!this.withinPageDrag_)
        this.updateTopMargin_();

      this.doDragOver(e);
    },

    /**
     * Performs all actions necessary when the user moves the cursor during
     * a drag over the tile page.
     * @param {Event} e A mouseover event for the drag over.
     */
    doDragOver: function(e) {
      e.preventDefault();

      this.setDropEffect(e.dataTransfer);
      var newDragIndex = this.getWouldBeIndexForPoint_(e.pageX, e.pageY);
      if (newDragIndex < 0 || newDragIndex >= this.tileElements_.length)
        newDragIndex = this.dragItemIndex_;
      this.updateDropIndicator_(newDragIndex);

      this.lastDropEffect_ = e.dataTransfer.dropEffect;
    },

    /**
     * Performs all actions necessary when the user completes a drop.
     * @param {Event} e A mouseover event for the drag drop.
     */
    doDrop: function(e) {
      e.stopPropagation();

      var index = this.currentDropIndex_;
      // Only change data if this was not a 'null drag'.
      if (!((index == this.dragItemIndex_) && this.withinPageDrag_)) {
        var adjustedIndex = this.currentDropIndex_ +
            (index > this.dragItemIndex_ ? 1 : 0);
        if (this.withinPageDrag_) {
          this.tileGrid_.insertBefore(
              currentlyDraggingTile,
              this.tileElements_[adjustedIndex]);
          this.tileMoved(currentlyDraggingTile, this.dragItemIndex_);
        } else {
          var originalPage = currentlyDraggingTile ?
              currentlyDraggingTile.tilePage : null;
          this.addDragData(e.dataTransfer, adjustedIndex);
          if (originalPage)
            originalPage.cleanupDrag();
        }

        // Dropping the icon may cause topMargin to change, but changing it
        // now would cause everything to move (annoying), so we leave it
        // alone. The top margin will be re-calculated next time the window is
        // resized or the page is selected.
      }

      this.classList.remove('animating-tile-page');
      this.cleanupDrag();
    },

    /**
     * Appends the currently dragged tile to the end of the page. Called
     * from outside the page, e.g. when dropping on a nav dot.
     */
    appendDraggingTile: function() {
      var originalPage = currentlyDraggingTile.tilePage;
      if (originalPage == this)
        return;

      this.addDragData(null, this.tileElements_.length);
      originalPage.cleanupDrag();
    },

    /**
     * Makes sure all the tiles are in the right place after a drag is over.
     */
    cleanupDrag: function() {
      this.repositionTiles_(currentlyDraggingTile);
      // Remove the drag mask.
      this.updateMask_();
    },

    /**
     * Reposition all the tiles (possibly ignoring one).
     * @param {Node} ignoreNode An optional node to ignore.
     * @private
     */
    repositionTiles_: function(ignoreNode) {
      for (var i = 0; i < this.tileElements_.length; i++) {
        if (!ignoreNode || ignoreNode !== this.tileElements_[i])
          this.positionTile_(i);
      }
    },

    /**
     * Updates the visual indicator for the drop location for the active drag.
     * @param {Event} e A MouseEvent for the drag.
     * @private
     */
    updateDropIndicator_: function(newDragIndex) {
      var oldDragIndex = this.currentDropIndex_;
      if (newDragIndex == oldDragIndex)
        return;

      var repositionStart = Math.min(newDragIndex, oldDragIndex);
      var repositionEnd = Math.max(newDragIndex, oldDragIndex);

      for (var i = repositionStart; i <= repositionEnd; i++) {
        if (i == this.dragItemIndex_)
          continue;
        else if (i > this.dragItemIndex_)
          var adjustment = i <= newDragIndex ? -1 : 0;
        else
          var adjustment = i >= newDragIndex ? 1 : 0;

        this.positionTile_(i, adjustment);
      }
      this.currentDropIndex_ = newDragIndex;
    },

    /**
     * Checks if a page can accept a drag with the given data.
     * @param {Event} e The drag event if the drag object. Implementations will
     *     likely want to check |e.dataTransfer|.
     * @return {boolean} True if this page can handle the drag.
     */
    shouldAcceptDrag: function(e) {
      return false;
    },

    /**
     * Called to accept a drag drop. Will not be called for in-page drops.
     * @param {Object} dataTransfer The data transfer object that holds the drop
     *     data. This should only be used if currentlyDraggingTile is null.
     * @param {number} index The tile index at which the drop occurred.
     */
    addDragData: function(dataTransfer, index) {
      assert(false);
    },

    /**
     * Called when a tile has been moved (via dragging). Override this to make
     * backend updates.
     * @param {Node} draggedTile The tile that was dropped.
     * @param {number} prevIndex The previous index of the tile.
     */
    tileMoved: function(draggedTile, prevIndex) {
    },

    /**
     * Sets the drop effect on |dataTransfer| to the desired value (e.g.
     * 'copy').
     * @param {Object} dataTransfer The drag event dataTransfer object.
     */
    setDropEffect: function(dataTransfer) {
      assert(false);
    },
  };

  return {
    getCurrentlyDraggingTile: getCurrentlyDraggingTile,
    TilePage: TilePage,
  };
});
</script>
<script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

cr.define('ntp4', function() {
  'use strict';

  var localStrings = new LocalStrings;

  var APP_LAUNCH = {
    // The histogram buckets (keep in sync with extension_constants.h).
    NTP_APPS_MAXIMIZED: 0,
    NTP_APPS_COLLAPSED: 1,
    NTP_APPS_MENU: 2,
    NTP_MOST_VISITED: 3,
    NTP_RECENTLY_CLOSED: 4,
    NTP_APP_RE_ENABLE: 16
  };

  /**
   * App context menu. The class is designed to be used as a singleton with
   * the app that is currently showing a context menu stored in this.app_.
   * @constructor
   */
  function AppContextMenu() {
    this.__proto__ = AppContextMenu.prototype;
    this.initialize();
  };
  cr.addSingletonGetter(AppContextMenu);

  AppContextMenu.prototype = {
    initialize: function() {
      var menu = new cr.ui.Menu;
      cr.ui.decorate(menu, cr.ui.Menu);
      menu.classList.add('app-context-menu');
      this.menu = menu;

      this.launch_ = this.appendMenuItem_();
      this.launch_.addEventListener('activate', this.onLaunch_.bind(this));

      menu.appendChild(cr.ui.MenuItem.createSeparator());
      this.launchRegularTab_ = this.appendMenuItem_('applaunchtyperegular');
      this.launchPinnedTab_ = this.appendMenuItem_('applaunchtypepinned');
      if (!cr.isMac)
        this.launchNewWindow_ = this.appendMenuItem_('applaunchtypewindow');
      this.launchFullscreen_ = this.appendMenuItem_('applaunchtypefullscreen');

      var self = this;
      this.forAllLaunchTypes_(function(launchTypeButton, id) {
        launchTypeButton.addEventListener('activate',
            self.onLaunchTypeChanged_.bind(self));
      });

      menu.appendChild(cr.ui.MenuItem.createSeparator());
      this.options_ = this.appendMenuItem_('appoptions');
      this.uninstall_ = this.appendMenuItem_('appuninstall');
      this.options_.addEventListener('activate',
                                     this.onShowOptions_.bind(this));
      this.uninstall_.addEventListener('activate',
                                       this.onUninstall_.bind(this));

      if (!cr.isMac && !cr.isChromeOS) {
        menu.appendChild(cr.ui.MenuItem.createSeparator());
        this.createShortcut_ = this.appendMenuItem_('appcreateshortcut');
        this.createShortcut_.addEventListener(
            'activate', this.onCreateShortcut_.bind(this));
      }

      document.body.appendChild(menu);
    },

    /**
     * Appends a menu item to |this.menu|.
     * @param {?String} textId If non-null, the ID for the localized string
     *     that acts as the item's label.
     */
    appendMenuItem_: function(textId) {
      var button = cr.doc.createElement('button');
      this.menu.appendChild(button);
      cr.ui.decorate(button, cr.ui.MenuItem);
      if (textId)
        button.textContent = localStrings.getString(textId);
      return button;
    },

    /**
     * Iterates over all the launch type menu items.
     * @param {function(cr.ui.MenuItem, number)} f The function to call for each
     *     menu item. The parameters to the function include the menu item and
     *     the associated launch ID.
     */
    forAllLaunchTypes_: function(f) {
      // Order matters: index matches launchType id.
      var launchTypes = [ this.launchPinnedTab_,
                          this.launchRegularTab_,
                          this.launchFullscreen_,
                          this.launchNewWindow_ ];

      for (var i = 0; i < launchTypes.length; ++i) {
        if (!launchTypes[i])
          continue;

        f(launchTypes[i], i);
      }
    },

    /**
     * Does all the necessary setup to show the menu for the give app.
     * @param {App} app The App object that will be showing a context menu.
     */
    setupForApp: function(app) {
      this.app_ = app;

      this.launch_.textContent = app.appData.name;

      this.forAllLaunchTypes_(function(launchTypeButton, id) {
        launchTypeButton.disabled = false;
        launchTypeButton.checked = app.appData.launch_type == id;
      });

      this.options_.disabled = !app.appData.options_url || !app.appData.enabled;
      this.uninstall_.disabled = !app.appData.can_uninstall;
    },

    /**
     * Handlers for menu item activation.
     * @param {Event} e The activation event.
     * @private
     */
    onLaunch_: function(e) {
      chrome.send('launchApp', [this.app_.appId, APP_LAUNCH.NTP_APPS_MENU]);
    },
    onLaunchTypeChanged_: function(e) {
      var pressed = e.currentTarget;
      var app = this.app_;
      this.forAllLaunchTypes_(function(launchTypeButton, id) {
        if (launchTypeButton == pressed) {
          chrome.send('setLaunchType', [app.appId, id]);
          // Manually update the launch type. We will only get
          // appsPrefChangedCallback calls after changes to other NTP instances.
          app.appData.launch_type = id;
        }
      });
    },
    onShowOptions_: function(e) {
      window.location = this.app_.appData.options_url;
    },
    onUninstall_: function(e) {
      chrome.send('uninstallApp', [this.app_.appData.id]);
    },
    onCreateShortcut_: function(e) {
      chrome.send('createAppShortcut', [this.app_.appData.id]);
    },
  };

  /**
   * Creates a new App object.
   * @param {Object} appData The data object that describes the app.
   * @constructor
   * @extends {HTMLDivElement}
   */
  function App(appData) {
    var el = cr.doc.createElement('div');
    el.__proto__ = App.prototype;
    el.appData = appData;
    el.initialize();

    return el;
  }

  App.prototype = {
    __proto__: HTMLDivElement.prototype,

    initialize: function() {
      assert(this.appData_.id, 'Got an app without an ID');
      this.id = this.appData_.id;

      this.className = 'app';

      var appContents = this.ownerDocument.createElement('div');
      appContents.className = 'app-contents';

      var appImgContainer = this.ownerDocument.createElement('div');
      appImgContainer.className = 'app-img-container';
      this.appImgContainer_ = appImgContainer;

      if (!this.appData_.icon_big_exists && this.appData_.icon_small_exists)
        this.useSmallIcon_ = true;

      var appImg = this.ownerDocument.createElement('img');
      // This is temporary (setIcon/loadIcon will overwrite it) but is visible
      // before the page is shown (e.g. if switching from most visited to
      // bookmarks).
      appImg.src = 'chrome://theme/IDR_APP_DEFAULT_ICON';
      this.appImg_ = appImg;
      this.setIcon();
      appImgContainer.appendChild(appImg);

      if (this.useSmallIcon_) {
        var imgDiv = this.ownerDocument.createElement('div');
        imgDiv.className = 'app-icon-div';
        imgDiv.appendChild(appImgContainer);
        imgDiv.addEventListener('click', this.onClick_.bind(this));
        imgDiv.title = this.appData_.name;
        this.imgDiv_ = imgDiv;
        appContents.appendChild(imgDiv);
        this.appImgContainer_.style.position = 'absolute';
        this.appImgContainer_.style.bottom = '10px';
        this.appImgContainer_.style.left = '10px';
        var stripeDiv = this.ownerDocument.createElement('div');
        stripeDiv.className = 'color-stripe';
        imgDiv.appendChild(stripeDiv);

        chrome.send('getAppIconDominantColor', [this.id]);
      } else {
        appImgContainer.addEventListener('click', this.onClick_.bind(this));
        appImgContainer.title = this.appData_.name;
        appContents.appendChild(appImgContainer);
      }

      var appSpan = this.ownerDocument.createElement('span');
      appSpan.textContent = appSpan.title = this.appData_.name;
      appSpan.addEventListener('click', this.onClick_.bind(this));
      appContents.appendChild(appSpan);
      this.appendChild(appContents);

      var notification = this.appData_.notification;
      var hasNotification = typeof notification != 'undefined' &&
                            typeof notification['title'] != 'undefined' &&
                            typeof notification['body'] != 'undefined';
      if (hasNotification)
        this.setupNotification_(notification);

      this.appContents_ = appContents;

      this.addEventListener('keydown', cr.ui.contextMenuHandler);
      this.addEventListener('keyup', cr.ui.contextMenuHandler);

      // This hack is here so that appContents.contextMenu will be the same as
      // this.contextMenu.
      var self = this;
      appContents.__defineGetter__('contextMenu', function() {
        return self.contextMenu;
      });
      appContents.addEventListener('contextmenu', cr.ui.contextMenuHandler);

      this.isStore_ = this.appData_.is_webstore;
      if (this.isStore_)
        this.createAppsPromoExtras_();

      this.addEventListener('mousedown', this.onMousedown_, true);
    },

    /**
     * Sets the color of the favicon dominant color bar.
     * @param {string} color The css-parsable value for the color.
     */
    set stripeColor(color) {
      this.querySelector('.color-stripe').style.backgroundColor = color;
    },

    /**
     * Removes the app tile from the page. Should be called after the app has
     * been uninstalled.
     */
    remove: function() {
      // Unset the ID immediately, because the app is already gone. But leave
      // the tile on the page as it animates out.
      this.id = '';
      this.tile.doRemove();
    },

    /**
     * Set the URL of the icon from |appData_|. This won't actually show the
     * icon until loadIcon() is called (for performance reasons; we don't want
     * to load icons until we have to).
     */
    setIcon: function() {
      var src = this.useSmallIcon_ ? this.appData_.icon_small :
                                     this.appData_.icon_big;
      if (!this.appData_.enabled ||
          (!this.appData_.offline_enabled && !navigator.onLine)) {
        src += '?grayscale=true';
      }

      this.appImgSrc_ = src;
      this.classList.add('default-icon');
    },

    /**
     * Shows the icon for the app. That is, it causes chrome to load the app
     * icon resource.
     */
    loadIcon: function() {
      if (this.appImgSrc_)
        this.appImg_.src = this.appImgSrc_;
      this.appImgSrc_ = null;
      this.classList.remove('default-icon');
    },

    // Shows a notification text below the app icon and stuffs the attributes
    // necessary to show the bubble when the user clicks on the notification
    // text.
    setupNotification_: function(notification) {
      // Remove the old notification from this node (if any).
      if (this.appNotification_)
        this.appNotification_.parentNode.removeChild(this.appNotification_);

      if (notification) {
        // Add a new notification to this node.
        var appNotification = this.ownerDocument.createElement('span');
        appNotification.className = 'app-notification';
        appNotification.textContent = notification['title'];
        appNotification.addEventListener('click',
                                         this.onNotificationClick_.bind(this));
        appNotification.notificationTitle = notification['title'];
        appNotification.notificationMessage = notification['body'];
        if (typeof notification['linkUrl'] != 'undefined' &&
            typeof notification['linkText'] != 'undefined') {
          appNotification.notificationLink = notification['linkUrl'];
          appNotification.notificationLinkText = notification['linkText'];
        }
        this.appNotification_ = appNotification;
        this.appendChild(appNotification);
      }
    },

    /**
     * Creates the apps-promo section of the app (should only be called for the
     * webstore app).
     * @private
     */
    createAppsPromoExtras_: function() {
      this.classList.add('webstore');

      this.appsPromoExtras_ = $('apps-promo-extras-template').cloneNode(true);
      this.appsPromoExtras_.id = '';
      this.appsPromoHeading_ =
          this.appsPromoExtras_.querySelector('.apps-promo-heading');
      this.appsPromoLink_ =
          this.appsPromoExtras_.querySelector('.apps-promo-link');

      this.appsPromoLogo_ = this.ownerDocument.createElement('img');
      this.appsPromoLogo_.className = 'apps-promo-logo';
      this.appImgContainer_.appendChild(this.appsPromoLogo_);

      this.appendChild(this.appsPromoExtras_);
      this.appsPromoExtras_.hidden = false;
    },

    /**
     * Sets the apps promo appearance. If |data| is null, there is no promo. If
     * |data| is non-null, it contains strings to be shown for the promo. The
     * promo is only shown when the webstore app icon is alone on a page.
     * @param {Object} data A dictionary that contains apps promo strings.
     */
    setAppsPromoData: function(data) {
      if (data) {
        this.classList.add('has-promo');
      } else {
        this.classList.remove('has-promo');
        return;
      }

      this.appsPromoHeading_.textContent = data.promoHeader;
      this.appsPromoLink_.href = data.promoLink;
      this.appsPromoLink_.textContent = data.promoButton;
      this.appsPromoLogo_.src = data.promoLogo;
    },

    /**
     * Set the size and position of the app tile.
     * @param {number} size The total size of |this|.
     * @param {number} x The x-position.
     * @param {number} y The y-position.
     *     animate.
     */
    setBounds: function(size, x, y) {
      var imgSize = size * APP_IMG_SIZE_FRACTION;
      this.appImgContainer_.style.width = this.appImgContainer_.style.height =
          this.useSmallIcon_ ? '16px' : imgSize + 'px';
      if (this.useSmallIcon_) {
        // 3/4 is the ratio of 96px to 128px (the used height and full height
        // of icons in apps).
        var iconSize = imgSize * 3/4;
        // The -2 is for the div border to improve the visual alignment for the
        // icon div.
        this.imgDiv_.style.width = this.imgDiv_.style.height =
            (iconSize - 2) + 'px';
        // Margins set to get the icon placement right and the text to line up.
        this.imgDiv_.style.marginTop = this.imgDiv_.style.marginBottom =
            ((imgSize - iconSize) / 2) + 'px';
      }

      this.style.width = this.style.height = size + 'px';
      if (this.isStore_)
        this.appsPromoExtras_.style.left = size + (imgSize - size) / 2 + 'px';

      this.style.left = x + 'px';
      this.style.right = x + 'px';
      this.style.top = y + 'px';
    },

    /**
     * Invoked when an app is clicked.
     * @param {Event} e The click event.
     * @private
     */
    onClick_: function(e) {
      chrome.send('launchApp',
                  [this.appId, APP_LAUNCH.NTP_APPS_MAXIMIZED,
                   e.altKey, e.ctrlKey, e.metaKey, e.shiftKey, e.button]);

      // Don't allow the click to trigger a link or anything
      e.preventDefault();
    },

    /**
     * Invoked when an app notification is clicked. This will show the
     * notification bubble, containing the details of the notification.
     * @param {Event} e The click event.
     * @private
     */
    onNotificationClick_: function(e) {
      var title = this.appNotification_.notificationTitle;
      var message = this.appNotification_.notificationMessage;
      var link = this.appNotification_.notificationLink;
      var linkMessage = this.appNotification_.notificationLinkText;

      if (!title || !message)
        return;

      var container = this.ownerDocument.createElement('div');
      var titleItem = this.ownerDocument.createElement('strong');
      titleItem.textContent = title;
      container.appendChild(titleItem);
      var messageDiv = this.ownerDocument.createElement('div');
      messageDiv.textContent = message;
      container.appendChild(messageDiv);
      if (link && linkMessage) {
        var anchor = this.ownerDocument.createElement('a');
        anchor.href = link;
        anchor.textContent = linkMessage;
        container.appendChild(anchor);
      }

      var infoBubble = new cr.ui.Bubble;
      infoBubble.anchorNode = e.target;
      infoBubble.content = container;
      infoBubble.show();
    },

    /**
     * Handler for mousedown on the App. Adds a class that allows us to
     * not display as :active for right clicks and clicks on app notifications
     * (specifically, don't pulse on these occasions).
     * @param {Event} e The mousedown event.
     */
    onMousedown_: function(e) {
      if (e.button == 2 || e.target.classList.contains('app-notification'))
        this.classList.add('suppress-active');
      else
        this.classList.remove('suppress-active');
    },

    /**
     * Change the appData and update the appearance of the app.
     * @param {Object} appData The new data object that describes the app.
     */
    replaceAppData: function(appData) {
      this.appData_ = appData;
      this.setIcon();
      this.loadIcon();
    },

    /**
     * The data and preferences for this app.
     * @type {Object}
     */
    set appData(data) {
      this.appData_ = data;
    },
    get appData() {
      return this.appData_;
    },

    get appId() {
      return this.appData_.id;
    },

    /**
     * Returns a pointer to the context menu for this app. All apps share the
     * singleton AppContextMenu. This function is called by the
     * ContextMenuHandler in response to the 'contextmenu' event.
     * @type {cr.ui.Menu}
     */
    get contextMenu() {
      var menu = AppContextMenu.getInstance();
      menu.setupForApp(this);
      return menu.menu;
    },

    /**
     * Returns whether this element can be 'removed' from chrome (i.e. whether
     * the user can drag it onto the trash and expect something to happen).
     * @return {boolean} True if the app can be uninstalled.
     */
    canBeRemoved: function() {
      return this.appData_.can_uninstall;
    },

    /**
     * Uninstalls the app after it's been dropped on the trash.
     */
    removeFromChrome: function() {
      chrome.send('uninstallApp', [this.appData_.id, true]);

      this.tile.tilePage.cleanupDrag();
      this.tile.parentNode.removeChild(this.tile);
    },

    /**
     * Called when a drag is starting on the tile. Updates dataTransfer with
     * data for this tile.
     */
    setDragData: function(dataTransfer) {
      dataTransfer.setData('Text', this.appData_.name);
      dataTransfer.setData('URL', this.appData_.launch_url);
    },
  };

  var TilePage = ntp4.TilePage;

  // The fraction of the app tile size that the icon uses.
  var APP_IMG_SIZE_FRACTION = 4 / 5;

  var appsPageGridValues = {
    // The fewest tiles we will show in a row.
    minColCount: 3,
    // The most tiles we will show in a row.
    maxColCount: 6,

    // The smallest a tile can be.
    minTileWidth: 64 / APP_IMG_SIZE_FRACTION,
    // The biggest a tile can be.
    maxTileWidth: 128 / APP_IMG_SIZE_FRACTION,

    // The padding between tiles, as a fraction of the tile width.
    tileSpacingFraction: 1 / 8,
  };
  TilePage.initGridValues(appsPageGridValues);

  /**
   * Creates a new AppsPage object.
   * @constructor
   * @extends {TilePage}
   */
  function AppsPage() {
    var el = new TilePage(appsPageGridValues);
    el.__proto__ = AppsPage.prototype;
    el.initialize();

    return el;
  }

  AppsPage.prototype = {
    __proto__: TilePage.prototype,

    initialize: function() {
      this.classList.add('apps-page');

      this.addEventListener('cardselected', this.onCardSelected_);
    },

    /**
     * Creates an app DOM element and places it at the last position on the
     * page.
     * @param {Object} appData The data object that describes the app.
     * @param {?boolean} animate If true, the app tile plays an animation.
     */
    appendApp: function(appData, animate) {
      if (animate) {
        // Select the page and scroll all the way down so the animation is
        // visible.
        ntp4.getCardSlider().selectCardByValue(this);
        this.content_.scrollTop = this.content_.scrollHeight;
      }
      var app = new App(appData);
      if (this.classList.contains('selected-card'))
        app.loadIcon();
      this.appendTile(app, animate);
    },

    /**
     * Handler for 'cardselected' event, fired when |this| is selected. The
     * first time this is called, we load all the app icons.
     * @private
     */
    onCardSelected_: function(e) {
      var apps = this.querySelectorAll('.app.default-icon');
      for (var i = 0; i < apps.length; i++) {
        apps[i].loadIcon();
      }
    },

    /** @inheritdoc */
    doDragOver: function(e) {
      var tile = ntp4.getCurrentlyDraggingTile();
      if (tile && !tile.querySelector('.app')) {
        e.preventDefault();
        this.setDropEffect(e.dataTransfer);
      } else {
        TilePage.prototype.doDragOver.call(this, e);
      }
    },

    /** @inheritDoc */
    shouldAcceptDrag: function(e) {
      return ntp4.getCurrentlyDraggingTile() ||
          (e.dataTransfer && e.dataTransfer.types.indexOf('url') != -1);
    },

    /** @inheritDoc */
    addDragData: function(dataTransfer, index) {
      var currentlyDraggingTile = ntp4.getCurrentlyDraggingTile();
      if (currentlyDraggingTile) {
        var tileContents = currentlyDraggingTile.firstChild;
        if (tileContents.classList.contains('app')) {
          this.tileGrid_.insertBefore(
              currentlyDraggingTile,
              this.tileElements_[index]);
          this.tileMoved(currentlyDraggingTile);
        } else if (currentlyDraggingTile.querySelector('.most-visited')) {
          this.generateAppForLink(tileContents.data);
        }
      } else {
        this.addOutsideData_(dataTransfer, index);
      }
    },

    /**
     * Adds drag data that has been dropped from a source that is not a tile.
     * @param {Object} dataTransfer The data transfer object that holds drop
     *     data.
     * @param {number} index The index for the new data.
     * @private
     */
    addOutsideData_: function(dataTransfer, index) {
      var url = dataTransfer.getData('url');
      assert(url);

      // If the dataTransfer has html data, use that html's text contents as the
      // title of the new link.
      var html = dataTransfer.getData('text/html');
      var title;
      if (html) {
        // It's important that we don't attach this node to the document
        // because it might contain scripts.
        var node = this.ownerDocument.createElement('div');
        node.innerHTML = html;
        title = node.textContent;
      }

      // Make sure title is >=1 and <=45 characters for Chrome app limits.
      if (!title)
        title = url;
      if (title.length > 45)
        title = title.substring(0, 45);
      var data = {url: url, title: title};

      // Synthesize an app.
      this.generateAppForLink(data);
    },

    /**
     * Creates a new crx-less app manifest and installs it.
     * @param {Object} data The data object describing the link. Must have |url|
     *     and |title| members.
     * TODO(estade): pass along an index.
     */
    generateAppForLink: function(data) {
      assert(data.url != undefined);
      assert(data.title != undefined);
      var pageIndex = ntp4.getAppsPageIndex(this);
      chrome.send('generateAppForLink', [data.url, data.title, pageIndex]);
    },

    /** @inheritDoc */
    tileMoved: function(draggedTile) {
      if (!(draggedTile.firstChild instanceof App))
        return;

      var pageIndex = ntp4.getAppsPageIndex(this);
      chrome.send('setPageIndex', [draggedTile.firstChild.appId, pageIndex]);

      var appIds = [];
      for (var i = 0; i < this.tileElements_.length; i++) {
        var tileContents = this.tileElements_[i].firstChild;
        if (tileContents instanceof App)
          appIds.push(tileContents.appId);
      }

      chrome.send('reorderApps', [draggedTile.firstChild.appId, appIds]);
    },

    /** @inheritDoc */
    setDropEffect: function(dataTransfer) {
      var tile = ntp4.getCurrentlyDraggingTile();
      if (tile && tile.querySelector('.app'))
        dataTransfer.dropEffect = 'move';
      else
        dataTransfer.dropEffect = 'copy';
    },
  };

  AppsPage.setPromo = function(data) {
    var store = document.querySelector('.webstore');
    if (store)
      store.setAppsPromoData(data);
  };

  /**
   * Callback invoked by chrome whenever an app preference changes.
   * @param {Object} data An object with all the data on available
   *     applications.
   */
  function appsPrefChangeCallback(data) {
    for (var i = 0; i < data.apps.length; ++i) {
      $(data.apps[i].id).appData = data.apps[i];
    }
  }

  /**
   * Launches the specified app using the APP_LAUNCH_NTP_APP_RE_ENABLE
   * histogram. This should only be invoked from the AppLauncherHandler.
   * @param {String} appID The ID of the app.
   */
  function launchAppAfterEnable(appId) {
    chrome.send('launchApp', [appId, APP_LAUNCH.NTP_APP_RE_ENABLE]);
  };

  function appNotificationChanged(id, notification) {
    $(id).setupNotification_(notification);
  };

  return {
    APP_LAUNCH: APP_LAUNCH,
    appNotificationChanged: appNotificationChanged,
    AppsPage: AppsPage,
    appsPrefChangeCallback: appsPrefChangeCallback,
    launchAppAfterEnable: launchAppAfterEnable,
  };
});

// TODO(estade): update the content handlers to use ntp namespace instead of
// making these global.
var appNotificationChanged = ntp4.appNotificationChanged;
var appsPrefChangeCallback = ntp4.appsPrefChangeCallback;
var launchAppAfterEnable = ntp4.launchAppAfterEnable;
</script>
<script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

cr.define('ntp4', function() {
  'use strict';

  var localStrings = new LocalStrings;

  /**
   * A running count of bookmark tiles that we create so that each will have
   * a unique ID.
   */
  var tileId = 0;

  /**
   * The maximum number of tiles that we will display on this page. If there
   * are not enough spaces to show all bookmarks, we'll include a link to the
   * Bookmarks manager.
   * TODO(csilv): Eliminate the need for this restraint.
   * @type {number}
   * @const
   */
  var MAX_BOOKMARK_TILES = 18;

  /**
   * The root node's ID. We need this to determine removable items (direct
   * children aren't removable).
   * @type {number}
   * @const
   */
  var ROOT_NODE_ID = '0';

  /**
   * The bookmark bar's ID. We need this to allow combining the root and
   * bookmarks bars at the top level of the folder hierarchy/bookmarks page.
   * aren't removable).
   * @type {number}
   * @const
   */
  var BOOKMARKS_BAR_ID = '1';

  /**
   * Creates a new bookmark object.
   * @param {Object} data The url and title.
   * @constructor
   * @extends {HTMLDivElement}
   */
  function Bookmark(data) {
    var el = $('bookmark-template').cloneNode(true);
    el.__proto__ = Bookmark.prototype;
    el.initialize(data);

    return el;
  }

  Bookmark.prototype = {
    __proto__: HTMLDivElement.prototype,

    /**
     * Initialize the bookmark object.
     * @param {Object} data The bookmark data (url, title, etc).
     */
    initialize: function(data) {
      this.data = data;
      this.hidden = false;

      var id = tileId++;
      this.id = 'bookmark_tile_' + id;

      var title = this.querySelector('.title');
      title.textContent = this.data.title;

      // Sets the tooltip.
      this.title = this.data.title;

      if (this.data.url) {
        var button = this.querySelector('.button');
        button.href = title.href = this.data.url;
      }

      var faviconDiv = this.querySelector('.favicon');
      var faviconUrl;
      if (this.data.url) {
        faviconUrl = 'chrome://favicon/size/16/' + this.data.url;
        chrome.send('getFaviconDominantColor', [faviconUrl, this.id]);
      } else {
        faviconUrl = 'chrome://theme/IDR_BOOKMARK_BAR_FOLDER';
        // TODO(csilv): Should we vary this color by platform?
        this.stripeColor = '#919191';
      }
      faviconDiv.style.backgroundImage = url(faviconUrl);

      if (this.canBeRemoved())
        this.classList.add('removable');

      this.addEventListener('click', this.handleClick_.bind(this));
    },

    /**
     * Sets the color of the favicon dominant color bar.
     * @param {string} color The css-parsable value for the color.
     */
    set stripeColor(color) {
      this.querySelector('.color-stripe').style.backgroundColor = color;
    },

    /**
     * Set the size and position of the bookmark tile.
     * @param {number} size The total size of |this|.
     * @param {number} x The x-position.
     * @param {number} y The y-position.
     *     animate.
     */
    setBounds: function(size, x, y) {
      this.style.width = size + 'px';
      this.style.height = heightForWidth(size) + 'px';

      this.style.left = x + 'px';
      this.style.right = x + 'px';
      this.style.top = y + 'px';
    },

    /**
     * Set some data on the drag when it starts.
     * @param {DataTransfer} dataTransfer A data transfer object from the
     * current drag.
     */
    setDragData: function(dataTransfer) {
      // OS X requires extra data drag data to consider a drag useful, so we're
      // appending some semi-useful data at the end to force drags to work on
      // this OS. Don't use this data for anything -- this is just a hack to
      // ensure drag works the same as on other platforms.
      dataTransfer.setData('Text', this.data.id);
    },

    /**
     * Invoked when a bookmark is clicked
     * @param {Event} e The click event.
     * @private
     */
    handleClick_: function(e) {
      if (e.target.classList.contains('close-button')) {
        e.preventDefault();
        this.removeFromChrome();
      } else if (!this.data.url) {
        chrome.send('getBookmarksData', [this.data.id]);
        e.preventDefault();
      }
    },

    /** @inheritDoc */
    removeFromChrome: function() {
      chrome.send('removeBookmark', [this.data.id]);
    },

    /**
     * All bookmarks except for children of the root node.
     * @return {boolean} Whether or not the item can be removed.
     */
    canBeRemoved: function() {
      return this.data.parentId !== ROOT_NODE_ID;
    },
  };

  /**
   * Creates a new bookmark title object.
   * @param {Object} data The url and title.
   * @constructor
   * @extends {HTMLDivElement}
   */
  function BookmarkTitle(data) {
    var el = cr.doc.createElement('div');
    el.__proto__ = BookmarkTitle.prototype;
    el.initialize(data);

    return el;
  }

  BookmarkTitle.prototype = {
    __proto__: HTMLDivElement.prototype,

    /**
     * Initialize the bookmark title object.
     */
    initialize: function(data) {
      this.className = 'title-crumb';
      this.folderId = data.id;
      this.textContent = data.parentId ? data.title :
          localStrings.getString('bookmarksPage');

      this.addEventListener('click', this.handleClick_);
    },

    /**
     * Invoked when a bookmark title is clicked
     * @param {Event} e The click event.
     * @private
     */
    handleClick_: function(e) {
      chrome.send('getBookmarksData', [this.folderId]);
    },
  };

  var TilePage = ntp4.TilePage;

  var bookmarksPageGridValues = {
    // The fewest tiles we will show in a row.
    minColCount: 3,
    // The most tiles we will show in a row.
    maxColCount: 6,

    // The smallest a tile can be.
    minTileWidth: 64,
    // The biggest a tile can be.
    maxTileWidth: 96,

    // The padding between tiles, as a fraction of the tile width.
    tileSpacingFraction: 1 / 2,
  };
  TilePage.initGridValues(bookmarksPageGridValues);

  /**
   * Calculates the height for a bookmarks tile for a given width. The size
   * is based on a desired size of 96x72 ratio.
   * @return {number} The height.
   */
  function heightForWidth(width) {
    // The 2s are for borders, the 31 is for the title.
    return (width - 2) * 72 / 96 + 2 + 31;
  }

  /**
   * Creates a new BookmarksPage object.
   * @constructor
   * @extends {TilePage}
   */
  function BookmarksPage() {
    var el = new TilePage(bookmarksPageGridValues);
    el.__proto__ = BookmarksPage.prototype;
    el.initialize();

    return el;
  }

  BookmarksPage.prototype = {
    __proto__: TilePage.prototype,

    /**
     * Initialize the bookmarks page object.
     */
    initialize: function() {
      this.classList.add('bookmarks-page');

      // Insert the bookmark titles header which is unique to bookmark pages.
      var titleWrapper = $('bookmarks-title-wrapper')
      titleWrapper.hidden = false;
      this.insertBefore(titleWrapper, this.firstChild);

      // Insert the top & bottom links for the Bookmarks Manager page.
      var pageContent = this.querySelector('.tile-page-content');
      var topWrapper = $('bookmarks-top-link-wrapper');
      pageContent.insertBefore(topWrapper, pageContent.firstChild);
      topWrapper.hidden = false;
      pageContent.appendChild($('bookmarks-bottom-link-wrapper'));
    },

    /**
     * Build the bookmark titles bar (ie, navigation hiearchy).
     * @param {Array} items The parent hiearchy of the current folder.
     * @private
     */
    updateBookmarkTitles_: function(items) {
      var wrapper = $('bookmarks-title-wrapper');
      var title = wrapper.querySelector('.section-title');
      title.innerHTML = '';

      for (var i = items.length - 1; i > 0; i--) {
        title.appendChild(new BookmarkTitle(items[i]));

        var separator = document.createElement('hr');
        separator.className = 'bookmark-separator';
        title.appendChild(separator);
      }

      var titleCrumb = new BookmarkTitle(items[0]);
      titleCrumb.classList.add('title-crumb-active');
      title.appendChild(titleCrumb);
    },

    /**
     * Build the bookmark tiles.
     * @param {Array} items The contents of the current folder.
     * @private
     */
    updateBookmarkTiles_: function(items) {
      this.removeAllTiles();
      var tile_count = Math.min(items.length, MAX_BOOKMARK_TILES);
      for (var i = 0; i < tile_count; i++)
        this.appendTile(new Bookmark(items[i]), false);

      var link = $('bookmarks-top-link-wrapper').querySelector('a');
      link.href = 'chrome://bookmarks/#' + this.id;

      var wrapper = $('bookmarks-bottom-link-wrapper');
      if (items.length > MAX_BOOKMARK_TILES) {
        var link = wrapper.querySelector('a');
        link.href = 'chrome://bookmarks/#' + this.id;
        wrapper.hidden = false;
      } else {
        wrapper.hidden = true;
      }
    },

    /**
     * Determine whether a bookmark ID matches a folder in the current
     * hierarchy.
     * @param {string} id The bookmark ID to search for.
     * @private
     */
    isBookmarkInParentHierarchy_: function(id) {
      var titlesWrapper = $('bookmarks-title-wrapper');
      var titles = titlesWrapper.querySelectorAll('.title-crumb');
      for (var i = 0; i < titles.length; i++) {
        var bookmarkTitle = titles[i];
        if (bookmarkTitle.folderId == id)
          return true;
      }
      return false;
    },

    /**
     * Tells if we're in currently in the given folder.
     * @param {String} id The folder node's ID.
     * @returns {boolean} If it's in that folder (visually).
     */
    currentlyInFolder_: function(id) {
      return id === this.id || (this.id === ROOT_NODE_ID &&
                                id === BOOKMARKS_BAR_ID);
    },

    /** @inheritDoc */
    shouldAcceptDrag: function(e) {
      var tile = ntp4.getCurrentlyDraggingTile();
      if (tile)
        return !!tile.querySelector('.most-visited, .bookmark');
      // If there was no dragging tile, look for a URL in the drag data.
      return e.dataTransfer && e.dataTransfer.types &&
             e.dataTransfer.types.indexOf('url') != -1;
    },

    /** @inheritDoc */
    heightForWidth: heightForWidth,

    /**
     * Invoked before a batch import begins.  We will ignore added/changed
     * notifications while the operation is in progress.
     */
    bookmarkImportBegan: function() {
      this.importing = true;
    },

    /**
     * Invoked after a batch import finishes.  We will reload the bookmarks
     * page to reflect the new state.
     */
    bookmarkImportEnded: function() {
      this.importing = false;
      chrome.send('getBookmarksData', []);
    },

    /**
     * Invoked when a node has been added.
     * @param {string} id The id of the newly created bookmark node.
     * @param {Object} bookmark The new bookmark node.
     * @param {boolean} fromCurrentPage True if the action was from this page.
     */
    bookmarkNodeAdded: function(id, bookmark, fromCurrentPage) {
      if (this.importing) return;
      if (this.currentlyInFolder_(bookmark.parentId)) {
        // If source of the add came from this page, show an animated insertion,
        // otherwise just quietly do it.
        this.addTileAt(new Bookmark(bookmark), bookmark.index, fromCurrentPage);
        this.repositionTiles_();
      }
    },

    /**
     * Invoked when the title or url of a node changes.
     * @param {string} id The id of the changed node.
     * @param {Object} changeInfo Details of the changed node.
     */
    bookmarkNodeChanged: function(id, changeInfo) {
      if (this.importing) return;

      // If the current folder or parent is being re-named, reload the page.
      // TODO(csilv): Optimize this to reload just the titles.
      if (this.isBookmarkInParentHierarchy_(id)) {
        chrome.send('getBookmarksData', [this.id]);
        return;
      }

      // If the target item is contained in this folder, update just that item.
      for (var i = 0; i < this.tiles.length; i++) {
        var tile = this.tiles[i];
        var data = tile.firstChild.data;

        if (data.id == id) {
          data.title = changeInfo.title;
          var title = tile.querySelector('.title');
          title.textContent = data.title;

          if (changeInfo.url) {
            data.url = changeInfo.url;
            var button = tile.querySelector('.button');
            button.href = title.href = data.url;
          }
          break;
        }
      }
    },

    /**
     * Invoked when the children (just direct children, not descendants) of
     * a folder have been reordered in some way, such as sorted.
     * @param {string} id The id of the reordered bookmark node.
     * @param {!Object} reorderInfo Details of the reordered bookmark node.
     */
    bookmarkNodeChildrenReordered: function(id, reorderInfo) {
      if (this.currentlyInFolder_(id))
        chrome.send('getBookmarksData', [this.id]);
    },

    /**
     * Invoked when a node has moved.
     * @param {string} id The id of the moved bookmark node.
     * @param {!Object} moveInfo Details of the moved bookmark.
     */
    bookmarkNodeMoved: function(id, moveInfo) {
      // TODO(csilv): Optimize this by doing less than reloading the folder.
      // Reload the current page if the target item is the current folder
      // or a parent of the current folder.
      if (this.isBookmarkInParentHierarchy_(id)) {
        chrome.send('getBookmarksData', [this.id]);
        return;
      }

      // Reload the current page if the target item is being moved to/from the
      // current folder.
      if (this.currentlyInFolder_(moveInfo.parentId) ||
          this.currentlyInFolder_(moveInfo.oldParentId)) {
        chrome.send('getBookmarksData', [this.id]);
      }
    },

    /**
     * Invoked when a node has been removed from a folder.
     * @param {string} id The id of the removed bookmark node.
     * @param {!Object} removeInfo Details of the removed bookmark node.
     * @param {boolearn} fromCurrentPage If the event was from this page.
     */
    bookmarkNodeRemoved: function(id, removeInfo, fromCurrentPage) {
      // If the target item is the visibile folder or a parent folder, load
      // the parent of the removed item.
      if (this.isBookmarkInParentHierarchy_(id)) {
        chrome.send('getBookmarksData', [removeInfo.parentId]);
        return;
      }

      // If the target item is contained in the visible folder, find the
      // matching tile and delete it.
      if (this.currentlyInFolder_(removeInfo.parentId)) {
        for (var i = 0; i < this.tiles.length; i++) {
          var tile = this.tiles[i];
          if (tile.firstChild.data.id == id) {
            this.removeTile(tile, fromCurrentPage);
            break;
          }
        }
      }
    },

    /**
     * Set the bookmark data that should be displayed, replacing any existing
     * data.
     * @param {Object} data Data that shoudl be displayed. Contains arrays
     *   'items' and 'navigationItems'.
     */
    set data(data) {
      this.id = data.navigationItems[0].id;
      this.updateBookmarkTiles_(data.items);
      this.updateBookmarkTitles_(data.navigationItems);
    },

    /** @inheritDoc */
    get extraBottomPadding() {
      return 40;
    },

    /** @inheritDoc */
    setDropEffect: function(dataTransfer) {
      var tile = ntp4.getCurrentlyDraggingTile();
      if (tile && tile.querySelector('.bookmark'))
        dataTransfer.dropEffect = 'move';
      else
        dataTransfer.dropEffect = 'copy';
    },

    /** @inheritDoc */
    addDragData: function(dataTransfer, index) {
      var parentId = ROOT_NODE_ID == this.id ? BOOKMARKS_BAR_ID : this.id;
      var currentlyDraggingTile = ntp4.getCurrentlyDraggingTile();
      if (currentlyDraggingTile) {
        var tileContents = currentlyDraggingTile.firstChild;
        if (tileContents.classList.contains('most-visited')) {
          this.generateBookmarkForLink(parentId, index,
                                       tileContents.textContent,
                                       tileContents.href);
        }
      } else {
        this.addOutsideData_(dataTransfer, parentId, index);
      }
    },

    /**
     * If we got an outside drag from a page or the URL bar.
     * @param {!DataTransfer} dataTransfer The drag data transfer object.
     * @param {number} parentId The parent ID for the current bookmark level.
     * @param {number} index Position it will be inserted relative to siblings.
     */
    addOutsideData_: function(dataTransfer, parentId, index) {
      var url = dataTransfer.getData('url');
      assert(url && url !== window.location.href);
      // Look for any text/html types first (links, etc.).
      var title, html = dataTransfer.getData('text/html');
      if (html) {
        // NOTE: Don't insert this into the DOM! It could XSS the page!
        var node = this.ownerDocument.createElement('div');
        node.innerHTML = html;
        var text = node.textContent, img = node.firstChild;
        // OS X prepends a <meta> tag to the html for some reason...
        if ('META' == img.nodeName)
          img = img.nextSibling;
        // Use the combined text (if it exists), otherwise fall back to an
        // image's alternate text (if they dragged an image onto this page).
        title = text || ('IMG' == img.nodeName && img.alt);
      }
      // If we *still* don't have a title, just use the URL.
      if (!title)
        title = url;
      // Create a bookmark for the dropped data.
      this.generateBookmarkForLink(parentId, index, title, url);
    },

    /**
     * Create a bookmark from a title/url.
     * @param {!string} parentId Stringified int64 of the parent node's ID.
     * @param {number} index Sibling relative index, i.e. 3rd on this level.
     * @param {string} title More human readable title of the bookmark.
     * @param {string} url URL of the bookmark to be created.
     */
    generateBookmarkForLink: function(parentId, index, title, url) {
      // Bookmark creation actually only *requires* a parent ID, as if we just
      // pass a parent ID it's created as a folder with blank title as a child
      // of that parent.
      assert(parentId);
      chrome.send('createBookmark', [parentId, index, title, url]);
    },

    /** @inheritDoc */
    tileMoved: function(tile, prevIndex) {
      var parentId = ROOT_NODE_ID == this.id ? BOOKMARKS_BAR_ID : this.id;
      chrome.send('moveBookmark', [tile.firstChild.data.id, parentId,
                                   tile.index + (prevIndex < tile.index)]);
    },
  };

  /**
   * Initializes and renders the bookmark chevron canvas.  This needs to be
   * performed after the page has been loaded so that we have access to the
   * style sheet values.
   */
  function initBookmarkChevron() {
    var wrapperStyle = window.getComputedStyle($('bookmarks-title-wrapper'));
    var width = 10;
    var height = parseInt(wrapperStyle.height, 10);
    var ctx = document.getCSSCanvasContext('2d', 'bookmark-chevron',
                                           width, height);
    ctx.strokeStyle = wrapperStyle.borderBottomColor;
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(width, height / 2);
    ctx.lineTo(0, height);
    ctx.stroke();
  }

  /**
   * Set the dominant color for a bookmark tile.  This is the callback method
   * from a request made when the tile was created.
   * @param {string} id The ID of the bookmark tile.
   * @param {string} color The color represented as a CSS string.
   */
  function setBookmarksFaviconDominantColor(id, color) {
    var tile = $(id);
    if (tile)
      tile.stripeColor = color;
  }

  return {
    BookmarksPage: BookmarksPage,
    initBookmarkChevron: initBookmarkChevron,
  };
});

window.addEventListener('load', ntp4.initBookmarkChevron);
</script>
<script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

cr.define('ntp4', function() {
  'use strict';

  var TilePage = ntp4.TilePage;

  /**
   */
  var tileID = 0;

  /**
   * Creates a new Most Visited object for tiling.
   * @constructor
   * @extends {HTMLAnchorElement}
   */
  function MostVisited() {
    var el = cr.doc.createElement('a');
    el.__proto__ = MostVisited.prototype;
    el.initialize();

    return el;
  }

  MostVisited.prototype = {
    __proto__: HTMLAnchorElement.prototype,

    initialize: function() {
      this.reset();

      this.addEventListener('click', this.handleClick_.bind(this));
      this.addEventListener('keydown', this.handleKeyDown_.bind(this));
    },

    get index() {
      assert(this.tile);
      return this.tile.index;
    },

    get data() {
      return this.data_;
    },

    /**
     * Clears the DOM hierarchy for this node, setting it back to the default
     * for a blank thumbnail.
     */
    reset: function() {
      this.className = 'most-visited filler real';
      this.innerHTML =
          '<span class="thumbnail-wrapper fills-parent">' +
            '<div class="close-button"></div>' +
            '<span class="thumbnail fills-parent">' +
              // thumbnail-shield provides a gradient fade effect.
              '<div class="thumbnail-shield fills-parent"></div>' +
            '</span>' +
            '<span class="favicon"></span>' +
          '</span>' +
          '<div class="color-stripe"></div>' +
          '<span class="title"></span>';

      this.removeAttribute('tabIndex');
      this.data_ = null;
      this.removeAttribute('id');
      this.title = '';
    },

    /**
     * Update the appearance of this tile according to |data|.
     * @param {Object} data A dictionary of relevant data for the page.
     */
    updateForData: function(data) {
      if (this.classList.contains('blacklisted') && data) {
        // Animate appearance of new tile.
        this.classList.add('new-tile-contents');
      }
      this.classList.remove('blacklisted');

      if (!data || data.filler) {
        if (this.data_)
          this.reset();
        return;
      }

      var id = tileID++;
      this.id = 'most-visited-tile-' + id;
      this.data_ = data;
      // TODO(estade): this shouldn't be focusable if the page isn't showing.
      this.tabIndex = 0;

      var faviconDiv = this.querySelector('.favicon');
      var faviconUrl = data.faviconUrl ||
          'chrome://favicon/size/16/' + data.url;
      faviconDiv.style.backgroundImage = url(faviconUrl);
      faviconDiv.dir = data.direction;
      if (data.faviconDominantColor)
        this.stripeColor = data.faviconDominantColor;
      else
        chrome.send('getFaviconDominantColor', [faviconUrl, this.id]);

      var title = this.querySelector('.title');
      title.textContent = data.title;
      title.dir = data.direction;

      // Sets the tooltip.
      this.title = data.title;

      var thumbnailUrl = data.thumbnailUrl || 'chrome://thumb/' + data.url;
      this.querySelector('.thumbnail').style.backgroundImage =
          url(thumbnailUrl);

      this.href = data.url;

      this.classList.remove('filler');
    },

    /**
     * Sets the color of the favicon dominant color bar.
     * @param {string} color The css-parsable value for the color.
     */
    set stripeColor(color) {
      this.querySelector('.color-stripe').style.backgroundColor = color;
    },

    /**
     * Handles a click on the tile.
     * @param {Event} e The click event.
     */
    handleClick_: function(e) {
      if (e.target.classList.contains('close-button')) {
        this.blacklist_();
        e.preventDefault();
      } else {
        // Records an app launch from the most visited page (Chrome will decide
        // whether the url is an app). TODO(estade): this only works for clicks;
        // other actions like "open in new tab" from the context menu won't be
        // recorded. Can this be fixed?
        chrome.send('recordAppLaunchByURL',
                    [encodeURIComponent(this.href),
                     ntp4.APP_LAUNCH.NTP_MOST_VISITED]);
        // Records the index of this tile.
        chrome.send('recordInHistogram', ['NTP_MostVisited', this.index, 8]);
      }
    },

    /**
     * Allow blacklisting most visited site using the keyboard.
     */
    handleKeyDown_: function(e) {
      if (!IS_MAC && e.keyCode == 46 || // Del
          IS_MAC && e.metaKey && e.keyCode == 8) { // Cmd + Backspace
        this.blacklist_();
      }
    },

    /**
     * Permanently removes a page from Most Visited.
     */
    blacklist_: function() {
      this.showUndoNotification_();
      chrome.send('blacklistURLFromMostVisited', [this.data_.url]);
      this.reset();
      chrome.send('getMostVisited');
      this.classList.add('blacklisted');
    },

    showUndoNotification_: function() {
      var data = this.data_;
      var self = this;
      var doUndo = function () {
        chrome.send('removeURLsFromMostVisitedBlacklist', [data.url]);
        self.updateForData(data);
      }

      var undo = {
        action: doUndo,
        text: templateData.undothumbnailremove,
      }

      var undoAll = {
        action: function() {
          chrome.send('clearMostVisitedURLsBlacklist', []);
        },
        text: templateData.restoreThumbnailsShort,
      }

      ntp4.showNotification(templateData.thumbnailremovednotification,
                            [undo, undoAll]);
    },

    /**
     * Set the size and position of the most visited tile.
     * @param {number} size The total size of |this|.
     * @param {number} x The x-position.
     * @param {number} y The y-position.
     *     animate.
     */
    setBounds: function(size, x, y) {
      this.style.width = size + 'px';
      this.style.height = heightForWidth(size) + 'px';

      this.style.left = x + 'px';
      this.style.right = x + 'px';
      this.style.top = y + 'px';
    },

    /**
     * Returns whether this element can be 'removed' from chrome (i.e. whether
     * the user can drag it onto the trash and expect something to happen).
     * @return {boolean} True, since most visited pages can always be
     *     blacklisted.
     */
    canBeRemoved: function() {
      return true;
    },

    /**
     * Removes this element from chrome, i.e. blacklists it.
     */
    removeFromChrome: function() {
      this.blacklist_();
      this.parentNode.classList.add('finishing-drag');
    },

    /**
     * Called when a drag of this tile has ended (after all animations have
     * finished).
     */
    finalizeDrag: function() {
      this.parentNode.classList.remove('finishing-drag');
    },

    /**
     * Called when a drag is starting on the tile. Updates dataTransfer with
     * data for this tile (for dragging outside of the NTP).
     */
    setDragData: function(dataTransfer) {
      dataTransfer.setData('Text', this.data_.title);
      dataTransfer.setData('URL', this.data_.url);
    },
  };

  var mostVisitedPageGridValues = {
    // The fewest tiles we will show in a row.
    minColCount: 2,
    // The most tiles we will show in a row.
    maxColCount: 4,

    // The smallest a tile can be.
    minTileWidth: 122,
    // The biggest a tile can be. 212 (max thumbnail width) + 2.
    maxTileWidth: 214,

    // The padding between tiles, as a fraction of the tile width.
    tileSpacingFraction: 1 / 8,
  };
  TilePage.initGridValues(mostVisitedPageGridValues);

  /**
   * Calculates the height for a Most Visited tile for a given width. The size
   * is based on the thumbnail, which should have a 212:132 ratio.
   * @return {number} The height.
   */
  function heightForWidth(width) {
    // The 2s are for borders, the 31 is for the title.
    return (width - 2) * 132 / 212 + 2 + 31;
  }

  var THUMBNAIL_COUNT = 8;

  /**
   * Creates a new MostVisitedPage object.
   * @constructor
   * @extends {TilePage}
   */
  function MostVisitedPage() {
    var el = new TilePage(mostVisitedPageGridValues);
    el.__proto__ = MostVisitedPage.prototype;
    el.initialize();

    return el;
  }

  MostVisitedPage.prototype = {
    __proto__: TilePage.prototype,

    initialize: function() {
      this.classList.add('most-visited-page');
      this.data_ = null;
      this.mostVisitedTiles_ = this.getElementsByClassName('most-visited real');
    },

    /**
     * Create blank (filler) tiles.
     * @private
     */
    createTiles_: function() {
      for (var i = 0; i < THUMBNAIL_COUNT; i++) {
        this.appendTile(new MostVisited());
      }
    },

    /**
     * Update the tiles after a change to |data_|.
     */
    updateTiles_: function() {
      for (var i = 0; i < THUMBNAIL_COUNT; i++) {
        var page = this.data_[i];
        var tile = this.mostVisitedTiles_[i];

        if (i >= this.data_.length)
          tile.reset();
        else
          tile.updateForData(page);
      }
    },

    /**
     * Array of most visited data objects.
     * @type {Array}
     */
    get data() {
      return this.data_;
    },
    set data(data) {
      var startTime = Date.now();

      // The first time data is set, create the tiles.
      if (!this.data_) {
        this.createTiles_();
        this.data_ = data.slice(0, THUMBNAIL_COUNT);
      } else {
        this.data_ = refreshData(this.data_, data);
      }

      this.updateTiles_();
      logEvent('mostVisited.layout: ' + (Date.now() - startTime));
    },

    /** @inheritDoc */
    shouldAcceptDrag: function(e) {
      return false;
    },

    /** @inheritDoc */
    heightForWidth: heightForWidth,
  };

  /**
   * We've gotten additional Most Visited data. Update our old data with the
   * new data. The ordering of the new data is not important, except when a
   * page is pinned. Thus we try to minimize re-ordering.
   * @param {Object} oldData The current Most Visited page list.
   * @param {Object} newData The new Most Visited page list.
   * @return The merged page list that should replace the current page list.
   */
  function refreshData(oldData, newData) {
    oldData = oldData.slice(0, THUMBNAIL_COUNT);
    newData = newData.slice(0, THUMBNAIL_COUNT);

    // Copy over pinned sites directly.
    for (var j = 0; j < newData.length; j++) {
      if (newData[j].pinned) {
        oldData[j] = newData[j];
        // Mark the entry as 'updated' so we don't try to update again.
        oldData[j].updated = true;
        // Mark the newData page as 'used' so we don't try to re-use it.
        newData[j].used = true;
      }
    }

    // Look through old pages; if they exist in the newData list, keep them
    // where they are.
    for (var i = 0; i < oldData.length; i++) {
      if (!oldData[i] || oldData[i].updated)
        continue;

      for (var j = 0; j < newData.length; j++) {
        if (newData[j].used)
          continue;

        if (newData[j].url == oldData[i].url) {
          // The background image and other data may have changed.
          oldData[i] = newData[j];
          oldData[i].updated = true;
          newData[j].used = true;
          break;
        }
      }
    }

    // Look through old pages that haven't been updated yet; replace them.
    for (var i = 0; i < oldData.length; i++) {
      if (oldData[i] && oldData[i].updated)
        continue;

      for (var j = 0; j < newData.length; j++) {
        if (newData[j].used)
          continue;

        oldData[i] = newData[j];
        oldData[i].updated = true;
        newData[j].used = true;
        break;
      }

      if (oldData[i] && !oldData[i].updated)
        oldData[i] = null;
    }

    // Clear 'updated' flags so this function will work next time it's called.
    for (var i = 0; i < THUMBNAIL_COUNT; i++) {
      if (oldData[i])
        oldData[i].updated = false;
    }

    return oldData;
  };

  return {
    MostVisitedPage: MostVisitedPage,
    refreshData: refreshData,
  };
});
</script>
<script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview Trash
 * This is the class for the trash can that appears when dragging an app.
 */

cr.define('ntp4', function() {
  'use strict';

  function Trash(trash) {
    trash.__proto__ = Trash.prototype;
    trash.initialize();
    return trash;
  }

  Trash.prototype = {
    __proto__: HTMLDivElement.prototype,

    initialize: function(element) {
      this.dragWrapper_ = new DragWrapper(this, this);
    },

    /**
     * Determines whether we are interested in the drag data for |e|.
     * @param {Event} e The event from drag enter.
     * @return {bool}
     */
    shouldAcceptDrag: function(e) {
      var tile = ntp4.getCurrentlyDraggingTile();
      if (!tile)
        return false;

      return tile.firstChild.canBeRemoved();
    },

    /**
     * Drag over handler.
     * @param {Event} e The drag event.
     */
    doDragOver: function(e) {
      ntp4.getCurrentlyDraggingTile().dragClone.classList.add(
          'hovering-on-trash');
      e.preventDefault();
      e.dataTransfer.dropEffect = 'move';
    },

    /**
     * Drag enter handler.
     * @param {Event} e The drag event.
     */
    doDragEnter: function(e) {
      this.doDragOver(e);
    },

    /**
     * Drop handler.
     * @param {Event} e The drag event.
     */
    doDrop: function(e) {
      e.preventDefault();

      var tile = ntp4.getCurrentlyDraggingTile();
      var page = tile.tilePage;
      tile.firstChild.removeFromChrome();
      tile.landedOnTrash = true;
      page.cleanupDrag();
    },

    /**
     * Drag leave handler.
     * @param {Event} e The drag event.
     */
    doDragLeave: function(e) {
      ntp4.getCurrentlyDraggingTile().dragClone.classList.remove(
          'hovering-on-trash');
    },
  };

  return {
    Trash: Trash,
  };
});
</script>
<script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview Page switcher
 * This is the class for the left and right navigation arrows that switch
 * between pages.
 */
cr.define('ntp4', function() {

  function PageSwitcher() {
  }

  PageSwitcher.template = {
    __proto__: HTMLButtonElement.prototype,

    decorate: function(el) {
      el.__proto__ = PageSwitcher.template;

      el.addEventListener('click', el.activate_);
      el.addEventListener('mousewheel', el.onMouseWheel_);

      el.direction_ = el.id == 'page-switcher-start' ? -1 : 1;

      el.dragWrapper_ = new DragWrapper(el, el);
    },

    /**
     * Activate the switcher (go to the next card).
     * @private
     */
    activate_: function() {
      var cardSlider = ntp4.getCardSlider();
      cardSlider.selectCard(cardSlider.currentCard + this.direction_, true);
    },

    /**
     * Handler for the mousewheel event on a pager. We pass through the scroll
     * to the page. This is necssary because the page is our sibling in the DOM
     * hierarchy, so the event won't naturally pass through to it.
     * @param {Event} e The mousewheel event.
     * @private
     */
    onMouseWheel_: function(e) {
      var page = ntp4.getCardSlider().currentCardValue;
      page.handleMouseWheel(e);
    },

    shouldAcceptDrag: function(e) {
      return true;
    },

    doDragEnter: function(e) {
      this.scheduleDelayedSwitch_();
      this.doDragOver(e);
    },

    doDragLeave: function(e) {
      this.cancelDelayedSwitch_();
    },

    doDragOver: function(e) {
      e.preventDefault();
      var targetPage = ntp4.getCardSlider().currentCardValue;
      targetPage.setDropEffect(e.dataTransfer);
    },

    doDrop: function(e) {
      e.stopPropagation();
      this.cancelDelayedSwitch_();

      var tile = ntp4.getCurrentlyDraggingTile();
      if (!tile)
        return;

      var sourcePage = tile.tilePage;
      var targetPage = ntp4.getCardSlider().currentCardValue;
      if (targetPage == sourcePage)
        return;

      targetPage.appendDraggingTile();
    },

    /**
     * Starts a timer to activate the switcher. The timer repeats until
     * cancelled by cancelDelayedSwitch_.
     * @private
     */
    scheduleDelayedSwitch_: function() {
      var self = this;
      function navPageClearTimeout() {
        self.activate_();
        self.dragNavTimeout_ = null;
        self.scheduleDelayedSwitch_();
      }
      this.dragNavTimeout_ = window.setTimeout(navPageClearTimeout, 500);
    },

    /**
     * Cancels the timer that activates the switcher while dragging.
     * @private
     */
    cancelDelayedSwitch_: function() {
      if (this.dragNavTimeout_) {
        window.clearTimeout(this.dragNavTimeout_);
        this.dragNavTimeout_ = null;
      }
    },

  };

  return {
    initializePageSwitcher: PageSwitcher.template.decorate
  }
});
</script>

<script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview Card slider implementation. Allows you to create interactions
 * that have items that can slide left to right to reveal additional items.
 * Works by adding the necessary event handlers to a specific DOM structure
 * including a frame, container and cards.
 * - The frame defines the boundary of one item. Each card will be expanded to
 *   fill the width of the frame. This element is also overflow hidden so that
 *   the additional items left / right do not trigger horizontal scrolling.
 * - The container is what all the touch events are attached to. This element
 *   will be expanded to be the width of all cards.
 * - The cards are the individual viewable items. There should be one card for
 *   each item in the list. Only one card will be visible at a time. Two cards
 *   will be visible while you are transitioning between cards.
 *
 * This class is designed to work well on any hardware-accelerated touch device.
 * It should still work on pre-hardware accelerated devices it just won't feel
 * very good. It should also work well with a mouse.
 */


// Use an anonymous function to enable strict mode just for this file (which
// will be concatenated with other files when embedded in Chrome
var CardSlider = (function() {
  'use strict';

  /**
   * @constructor
   * @param {!Element} frame The bounding rectangle that cards are visible in.
   * @param {!Element} container The surrounding element that will have event
   *     listeners attached to it.
   * @param {number} cardWidth The width of each card should have.
   */
  function CardSlider(frame, container, cardWidth) {
    /**
     * @type {!Element}
     * @private
     */
    this.frame_ = frame;

    /**
     * @type {!Element}
     * @private
     */
    this.container_ = container;

    /**
     * Array of card elements.
     * @type {!Array.<!Element>}
     * @private
     */
    this.cards_ = [];

    /**
     * Index of currently shown card.
     * @type {number}
     * @private
     */
    this.currentCard_ = 0;

    /**
     * @type {number}
     * @private
     */
    this.cardWidth_ = cardWidth;

    /**
     * @type {!TouchHandler}
     * @private
     */
    this.touchHandler_ = new TouchHandler(this.container_);

  }

  /**
   * Events fired by the slider.
   * Events are fired at the container.
   */
  CardSlider.EventType = {
    // Fired when the user slides to another card.
    CARD_CHANGED: 'cardSlider:card_changed'
  };


  /**
   * The time to transition between cards when animating. Measured in ms.
   * @type {number}
   * @private
   * @const
   */
  CardSlider.TRANSITION_TIME_ = 200;


  /**
   * The minimum velocity required to transition cards if they did not drag past
   * the halfway point between cards. Measured in pixels / ms.
   * @type {number}
   * @private
   * @const
   */
  CardSlider.TRANSITION_VELOCITY_THRESHOLD_ = 0.2;


  CardSlider.prototype = {
    /**
     * The current left offset of the container relative to the frame.
     * @type {number}
     * @private
     */
    currentLeft_: 0,

    /**
     * Initialize all elements and event handlers. Must call after construction
     * and before usage.
     */
    initialize: function() {
      var view = this.container_.ownerDocument.defaultView;
      assert(view.getComputedStyle(this.container_).display == '-webkit-box',
          'Container should be display -webkit-box.');
      assert(view.getComputedStyle(this.frame_).overflow == 'hidden',
          'Frame should be overflow hidden.');
      assert(view.getComputedStyle(this.container_).position == 'static',
          'Container should be position static.');

      this.updateCardWidths_();

      this.mouseWheelScrollAmount_ = 0;
      this.scrollClearTimeout_ = null;
      this.frame_.addEventListener('mousewheel',
                                   this.onMouseWheel_.bind(this));

      if (document.documentElement.getAttribute('touchui')) {
        this.container_.addEventListener(TouchHandler.EventType.TOUCH_START,
                                         this.onTouchStart_.bind(this));
        this.container_.addEventListener(TouchHandler.EventType.DRAG_START,
                                         this.onDragStart_.bind(this));
        this.container_.addEventListener(TouchHandler.EventType.DRAG_MOVE,
                                         this.onDragMove_.bind(this));
        this.container_.addEventListener(TouchHandler.EventType.DRAG_END,
                                         this.onDragEnd_.bind(this));

        this.touchHandler_.enable(/* opt_capture */ false);
      }
    },

    /**
     * Use in cases where the width of the frame has changed in order to update
     * the width of cards. For example should be used when orientation changes
     * in full width sliders.
     * @param {number} newCardWidth Width all cards should have, in pixels.
     */
    resize: function(newCardWidth) {
      if (newCardWidth != this.cardWidth_) {
        this.cardWidth_ = newCardWidth;

        this.updateCardWidths_();

        // Must upate the transform on the container to show the correct card.
        this.transformToCurrentCard_();
      }
    },

    /**
     * Sets the cards used. Can be called more than once to switch card sets.
     * @param {!Array.<!Element>} cards The individual viewable cards.
     * @param {number} index Index of the card to in the new set of cards to
     *     navigate to.
     */
    setCards: function(cards, index) {
      assert(index >= 0 && index < cards.length,
          'Invalid index in CardSlider#setCards');
      this.cards_ = cards;

      this.updateCardWidths_();

      // Jump to the given card index.
      this.selectCard(index);
    },

    /**
     * Updates the width of each card.
     * @private
     */
    updateCardWidths_: function() {
      for (var i = 0, card; card = this.cards_[i]; i++)
        card.style.width = this.cardWidth_ + 'px';
    },

    /**
     * Returns the index of the current card.
     * @return {number} index of the current card.
     */
    get currentCard() {
      return this.currentCard_;
    },

    /**
     * Allows setting the current card index.
     * @param {number} index A new index to set the current index to.
     * @return {number} The new index after having been set.
     */
    set currentCard(index) {
      return (this.currentCard_ = index);
    },

    /**
     * Returns the number of cards.
     * @return {number} number of cards.
     */
    get cardCount() {
      return this.cards_.length;
    },

    /**
     * Returns the current card itself.
     * @return {!Element} the currently shown card.
     */
    get currentCardValue() {
      return this.cards_[this.currentCard_];
    },

    /**
     * Handle horizontal scrolls to flip between pages.
     * @private
     */
    onMouseWheel_: function(e) {
      if (e.wheelDeltaX == 0)
        return;

      // Prevent OS X 10.7+ history swiping on the NTP.
      e.preventDefault();

      // Mac value feels ok with multitouch trackpads and magic mice
      // (with physical scrollwheel, too), but not so great with logitech
      // mice.
      var scrollAmountPerPage = cr.isMac ? 400 : 120;
      if (!ntp4.isRTL())
        scrollAmountPerPage *= -1;
      this.mouseWheelScrollAmount_ += e.wheelDeltaX;
      if (Math.abs(this.mouseWheelScrollAmount_) >=
          Math.abs(scrollAmountPerPage)) {
        var pagesToScroll = this.mouseWheelScrollAmount_ / scrollAmountPerPage;
        pagesToScroll =
            (pagesToScroll > 0 ? Math.floor : Math.ceil)(pagesToScroll);
        var newCardIndex = this.currentCard + pagesToScroll;
        newCardIndex = Math.min(this.cards_.length - 1,
                                Math.max(0, newCardIndex));
        this.selectCard(newCardIndex, true);
        this.mouseWheelScrollAmount_ -= pagesToScroll * scrollAmountPerPage;
      }

      // We got a mouse wheel event, so cancel any pending scroll wheel timeout.
      if (this.scrollClearTimeout_ != null)
        clearTimeout(this.scrollClearTimeout_);
      // If we didn't use up all the scroll, hold onto it for a little bit, but
      // drop it after a delay.
      if (this.mouseWheelScrollAmount_ != 0) {
        this.scrollClearTimeout_ =
            setTimeout(this.clearMouseWheelScroll_.bind(this), 500);
      }
    },

    /**
     * Resets the amount of horizontal scroll we've seen to 0. See
     * onMouseWheel_.
     * @private
     */
    clearMouseWheelScroll_: function() {
      this.mouseWheelScrollAmount_ = 0;
    },

    /**
     * Selects a new card, ensuring that it is a valid index, transforming the
     * view and possibly calling the change card callback.
     * @param {number} newCardIndex Index of card to show.
     * @param {boolean=} opt_animate If true will animate transition from
     *     current position to new position.
     */
    selectCard: function(newCardIndex, opt_animate) {
      var isChangingCard =
          !this.cards_[newCardIndex].classList.contains('selected-card');

      if (isChangingCard) {
        this.currentCardValue.classList.remove('selected-card');
        // If we have a new card index and it is valid then update the left
        // position and current card index.
        this.currentCard_ = newCardIndex;
        this.currentCardValue.classList.add('selected-card');
      }

      this.transformToCurrentCard_(opt_animate);

      if (isChangingCard) {
        var event = document.createEvent('Event');
        event.initEvent(CardSlider.EventType.CARD_CHANGED, true, true);
        event.cardSlider = this;
        this.container_.dispatchEvent(event);

        // We also dispatch an event on the card itself.
        cr.dispatchSimpleEvent(this.currentCardValue, 'cardselected',
                               true, true);
      }
    },

    /**
     * Selects a card from the stack. Passes through to selectCard.
     * @param {Node} newCard The card that should be selected.
     * @param {boolean=} opt_animate Whether to animate.
     */
    selectCardByValue: function(newCard, opt_animate) {
      var i = this.cards_.indexOf(newCard);
      assert(i != -1);
      this.selectCard(i, opt_animate);
    },

    /**
     * Centers the view on the card denoted by this.currentCard. Can either
     * animate to that card or snap to it.
     * @param {boolean=} opt_animate If true will animate transition from
     *     current position to new position.
     * @private
     */
    transformToCurrentCard_: function(opt_animate) {
      this.currentLeft_ = -this.cardWidth_ *
          (ntp4.isRTL() ? this.cards_.length - this.currentCard - 1 :
                          this.currentCard);

      // Animate to the current card, which will either transition if the
      // current card is new, or reset the existing card if we didn't drag
      // enough to change cards.
      var transition = '';
      if (opt_animate) {
        transition = '-webkit-transform ' + CardSlider.TRANSITION_TIME_ +
                     'ms ease-in-out';
      }
      this.container_.style.WebkitTransition = transition;
      this.translateTo_(this.currentLeft_);
    },

    /**
     * Moves the view to the specified position.
     * @param {number} x Horizontal position to move to.
     * @private
     */
    translateTo_: function(x) {
      // We use a webkitTransform to slide because this is GPU accelerated on
      // Chrome and iOS.  Once Chrome does GPU acceleration on the position
      // fixed-layout elements we could simply set the element's position to
      // fixed and modify 'left' instead.
      this.container_.style.WebkitTransform = 'translate3d(' + x + 'px, 0, 0)';
    },

    /* Touch ******************************************************************/

    /**
     * Clear any transition that is in progress and enable dragging for the
     * touch.
     * @param {!TouchHandler.Event} e The TouchHandler event.
     * @private
     */
    onTouchStart_: function(e) {
      this.container_.style.WebkitTransition = '';
      e.enableDrag = true;
    },

    /**
     * Tell the TouchHandler that dragging is acceptable when the user begins by
     * scrolling horizontally.
     * @param {!TouchHandler.Event} e The TouchHandler event.
     * @private
     */
    onDragStart_: function(e) {
      e.enableDrag = Math.abs(e.dragDeltaX) > Math.abs(e.dragDeltaY);
    },

    /**
     * On each drag move event reposition the container appropriately so the
     * cards look like they are sliding.
     * @param {!TouchHandler.Event} e The TouchHandler event.
     * @private
     */
    onDragMove_: function(e) {
      var deltaX = e.dragDeltaX;
      // If dragging beyond the first or last card then apply a backoff so the
      // dragging feels stickier than usual.
      if (!this.currentCard && deltaX > 0 ||
          this.currentCard == (this.cards_.length - 1) && deltaX < 0) {
        deltaX /= 2;
      }
      this.translateTo_(this.currentLeft_ + deltaX);
    },

    /**
     * On drag end events we may want to transition to another card, depending
     * on the ending position of the drag and the velocity of the drag.
     * @param {!TouchHandler.Event} e The TouchHandler event.
     * @private
     */
    onDragEnd_: function(e) {
      var deltaX = e.dragDeltaX;
      var velocity = this.touchHandler_.getEndVelocity().x;
      var newX = this.currentLeft_ + deltaX;
      var newCardIndex = Math.round(-newX / this.cardWidth_);

      if (newCardIndex == this.currentCard && Math.abs(velocity) >
          CardSlider.TRANSITION_VELOCITY_THRESHOLD_) {
        // If the drag wasn't far enough to change cards but the velocity was
        // high enough to transition anyways. If the velocity is to the left
        // (negative) then the user wishes to go right (card +1).
        newCardIndex += velocity > 0 ? -1 : 1;
      }

      this.selectCard(newCardIndex, /* animate */ true);
    },

    /**
     * Cancel any current touch/slide as if we saw a touch end
     */
    cancelTouch: function() {
      // Stop listening to any current touch
      this.touchHandler_.cancelTouch();

      // Ensure we're at a card bounary
      this.transformToCurrentCard_(true);
    },
  };

  return CardSlider;
})();
</script>
<script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview Grabber implementation.
 * Allows you to pick up objects (with a long-press) and drag them around the
 * screen.
 *
 * Note: This should perhaps really use standard drag-and-drop events, but there
 * is no standard for them on touch devices.  We could define a model for
 * activating touch-based dragging of elements (programatically and/or with
 * CSS attributes) and use it here (even have a JS library to generate such
 * events when the browser doesn't support them).
 */

// Use an anonymous function to enable strict mode just for this file (which
// will be concatenated with other files when embedded in Chrome)
var Grabber = (function() {
  'use strict';

  /**
   * Create a Grabber object to enable grabbing and dragging a given element.
   * @constructor
   * @param {!Element} element The element that can be grabbed and moved.
   */
  function Grabber(element) {
    /**
     * The element the grabber is attached to.
     * @type {!Element}
     * @private
     */
    this.element_ = element;

    /**
     * The TouchHandler responsible for firing lower-level touch events when the
     * element is manipulated.
     * @type {!TouchHandler}
     * @private
     */
    this.touchHandler_ = new TouchHandler(this.element);

    /**
     * Tracks all event listeners we have created.
     * @type {EventTracker}
     * @private
     */
    this.events_ = new EventTracker();

    // Enable the generation of events when the element is touched (but no need
    // to use the early capture phase of event processing).
    this.touchHandler_.enable(/* opt_capture */ false);

    // Prevent any built-in drag-and-drop support from activating for the
    // element. Note that we don't want details of how we're implementing
    // dragging here to leak out of this file (eg. we may switch to using webkit
    // drag-and-drop).
    this.events_.add(this.element, 'dragstart', function(e) {
      e.preventDefault();
    }, true);

    // Add our TouchHandler event listeners
    this.events_.add(this.element, TouchHandler.EventType.TOUCH_START,
        this.onTouchStart_.bind(this), false);
    this.events_.add(this.element, TouchHandler.EventType.LONG_PRESS,
        this.onLongPress_.bind(this), false);
    this.events_.add(this.element, TouchHandler.EventType.DRAG_START,
        this.onDragStart_.bind(this), false);
    this.events_.add(this.element, TouchHandler.EventType.DRAG_MOVE,
        this.onDragMove_.bind(this), false);
    this.events_.add(this.element, TouchHandler.EventType.DRAG_END,
        this.onDragEnd_.bind(this), false);
    this.events_.add(this.element, TouchHandler.EventType.TOUCH_END,
        this.onTouchEnd_.bind(this), false);
  }

  /**
   * Events fired by the grabber.
   * Events are fired at the element affected (not the element being dragged).
   * @enum {string}
   */
  Grabber.EventType = {
    // Fired at the grabber element when it is first grabbed
    GRAB: 'grabber:grab',
    // Fired at the grabber element when dragging begins (after GRAB)
    DRAG_START: 'grabber:dragstart',
    // Fired at an element when something is dragged over top of it.
    DRAG_ENTER: 'grabber:dragenter',
    // Fired at an element when something is no longer over top of it.
    // Not fired at all in the case of a DROP
    DRAG_LEAVE: 'grabber:drag',
    // Fired at an element when something is dropped on top of it.
    DROP: 'grabber:drop',
    // Fired at the grabber element when dragging ends (successfully or not) -
    // after any DROP or DRAG_LEAVE
    DRAG_END: 'grabber:dragend',
    // Fired at the grabber element when it is released (even if no drag
    // occured) - after any DRAG_END event.
    RELEASE: 'grabber:release'
  };

  /**
   * The type of Event sent by Grabber
   * @constructor
   * @param {string} type The type of event (one of Grabber.EventType).
   * @param {Element!} grabbedElement The element being dragged.
   */
  Grabber.Event = function(type, grabbedElement) {
    var event = document.createEvent('Event');
    event.initEvent(type, true, true);
    event.__proto__ = Grabber.Event.prototype;

    /**
     * The element which is being dragged.  For some events this will be the
     * same as 'target', but for events like DROP that are fired at another
     * element it will be different.
     * @type {!Element}
     */
    event.grabbedElement = grabbedElement;

    return event;
  };

  Grabber.Event.prototype = {
    __proto__: Event.prototype
  };


  /**
   * The CSS class to apply when an element is touched but not yet
   * grabbed.
   * @type {string}
   */
  Grabber.PRESSED_CLASS = 'grabber-pressed';

  /**
   * The class to apply when an element has been held (including when it is
   * being dragged.
   * @type {string}
   */
  Grabber.GRAB_CLASS = 'grabber-grabbed';

  /**
   * The class to apply when a grabbed element is being dragged.
   * @type {string}
   */
  Grabber.DRAGGING_CLASS = 'grabber-dragging';

  Grabber.prototype = {
    /**
     * @return {!Element} The element that can be grabbed.
     */
    get element() {
      return this.element_;
    },

    /**
     * Clean up all event handlers (eg. if the underlying element will be
     * removed)
     */
    dispose: function() {
      this.touchHandler_.disable();
      this.events_.removeAll();

      // Clean-up any active touch/drag
      if (this.dragging_)
        this.stopDragging_();
      this.onTouchEnd_();
    },

    /**
     * Invoked whenever this element is first touched
     * @param {!TouchHandler.Event} e The TouchHandler event.
     * @private
     */
    onTouchStart_: function(e) {
      this.element.classList.add(Grabber.PRESSED_CLASS);

      // Always permit the touch to perhaps trigger a drag
      e.enableDrag = true;
    },

    /**
     * Invoked whenever the element stops being touched.
     * Can be called explicitly to cleanup any active touch.
     * @param {!TouchHandler.Event=} opt_e The TouchHandler event.
     * @private
     */
    onTouchEnd_: function(opt_e) {
      if (this.grabbed_) {
        // Mark this element as no longer being grabbed
        this.element.classList.remove(Grabber.GRAB_CLASS);
        this.element.style.pointerEvents = '';
        this.grabbed_ = false;

        this.sendEvent_(Grabber.EventType.RELEASE, this.element);
      } else {
        this.element.classList.remove(Grabber.PRESSED_CLASS);
      }
    },

    /**
     * Handler for TouchHandler's LONG_PRESS event
     * Invoked when the element is held (without being dragged)
     * @param {!TouchHandler.Event} e The TouchHandler event.
     * @private
     */
    onLongPress_: function(e) {
      assert(!this.grabbed_, 'Got longPress while still being held');

      this.element.classList.remove(Grabber.PRESSED_CLASS);
      this.element.classList.add(Grabber.GRAB_CLASS);

      // Disable mouse events from the element - we care only about what's
      // under the element after it's grabbed (since we're getting move events
      // from the body - not the element itself).  Note that we can't wait until
      // onDragStart to do this because it won't have taken effect by the first
      // onDragMove.
      this.element.style.pointerEvents = 'none';

      this.grabbed_ = true;

      this.sendEvent_(Grabber.EventType.GRAB, this.element);
    },

    /**
     * Invoked when the element is dragged.
     * @param {!TouchHandler.Event} e The TouchHandler event.
     * @private
     */
    onDragStart_: function(e) {
      assert(!this.lastEnter_, 'only expect one drag to occur at a time');
      assert(!this.dragging_);

      // We only want to drag the element if its been grabbed
      if (this.grabbed_) {
        // Mark the item as being dragged
        // Ensures our translate transform won't be animated and cancels any
        // outstanding animations.
        this.element.classList.add(Grabber.DRAGGING_CLASS);

        // Determine the webkitTransform currently applied to the element.
        // Note that it's important that we do this AFTER cancelling animation,
        // otherwise we could see an intermediate value.
        // We'll assume this value will be constant for the duration of the drag
        // so that we can combine it with our translate3d transform.
        this.baseTransform_ = this.element.ownerDocument.defaultView.
            getComputedStyle(this.element).webkitTransform;

        this.sendEvent_(Grabber.EventType.DRAG_START, this.element);
        e.enableDrag = true;
        this.dragging_ = true;

      } else {
        // Hasn't been grabbed - don't drag, just unpress
        this.element.classList.remove(Grabber.PRESSED_CLASS);
        e.enableDrag = false;
      }
    },

    /**
     * Invoked when a grabbed element is being dragged
     * @param {!TouchHandler.Event} e The TouchHandler event.
     * @private
     */
    onDragMove_: function(e) {
      assert(this.grabbed_ && this.dragging_);

      this.translateTo_(e.dragDeltaX, e.dragDeltaY);

      var target = e.touchedElement;
      if (target && target != this.lastEnter_) {
        // Send the events
        this.sendDragLeave_(e);
        this.sendEvent_(Grabber.EventType.DRAG_ENTER, target);
      }
      this.lastEnter_ = target;
    },

    /**
     * Send DRAG_LEAVE to the element last sent a DRAG_ENTER if any.
     * @param {!TouchHandler.Event} e The event triggering this DRAG_LEAVE.
     * @private
     */
    sendDragLeave_: function(e) {
      if (this.lastEnter_) {
        this.sendEvent_(Grabber.EventType.DRAG_LEAVE, this.lastEnter_);
        this.lastEnter_ = undefined;
      }
    },

    /**
     * Moves the element to the specified position.
     * @param {number} x Horizontal position to move to.
     * @param {number} y Vertical position to move to.
     * @private
     */
    translateTo_: function(x, y) {
      // Order is important here - we want to translate before doing the zoom
      this.element.style.WebkitTransform = 'translate3d(' + x + 'px, ' +
          y + 'px, 0) ' + this.baseTransform_;
    },

    /**
     * Invoked when the element is no longer being dragged.
     * @param {TouchHandler.Event} e The TouchHandler event.
     * @private
     */
    onDragEnd_: function(e) {
      // We should get this before the onTouchEnd.  Don't change
      // this.grabbed_ - it's onTouchEnd's responsibility to clear it.
      assert(this.grabbed_ && this.dragging_);
      var event;

      // Send the drop event to the element underneath the one we're dragging.
      var target = e.touchedElement;
      if (target)
        this.sendEvent_(Grabber.EventType.DROP, target);

      // Cleanup and send DRAG_END
      // Note that like HTML5 DND, we don't send DRAG_LEAVE on drop
      this.stopDragging_();
    },

    /**
     * Clean-up the active drag and send DRAG_LEAVE
     * @private
     */
    stopDragging_: function() {
      assert(this.dragging_);
      this.lastEnter_ = undefined;

      // Mark the element as no longer being dragged
      this.element.classList.remove(Grabber.DRAGGING_CLASS);
      this.element.style.webkitTransform = '';

      this.dragging_ = false;
      this.sendEvent_(Grabber.EventType.DRAG_END, this.element);
    },

    /**
     * Send a Grabber event to a specific element
     * @param {string} eventType The type of event to send.
     * @param {!Element} target The element to send the event to.
     * @private
     */
    sendEvent_: function(eventType, target) {
      var event = new Grabber.Event(eventType, this.element);
      target.dispatchEvent(event);
    },

    /**
     * Whether or not the element is currently grabbed.
     * @type {boolean}
     * @private
     */
    grabbed_: false,

    /**
     * Whether or not the element is currently being dragged.
     * @type {boolean}
     * @private
     */
    dragging_: false,

    /**
     * The webkitTransform applied to the element when it first started being
     * dragged.
     * @type {string|undefined}
     * @private
     */
    baseTransform_: undefined,

    /**
     * The element for which a DRAG_ENTER event was last fired
     * @type {Element|undefined}
     * @private
     */
    lastEnter_: undefined
  };

  return Grabber;
})();
</script>
<script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview Nav dot
 * This is the class for the navigation controls that appear along the bottom
 * of the NTP.
 */

cr.define('ntp4', function() {
  'use strict';

  /**
   * Creates a new navigation dot.
   * @param {TilePage} page The associated TilePage.
   * @param {string} title The title of the navigation dot.
   * @param {bool} titleIsEditable If true, the title can be changed.
   * @param {bool} animate If true, animates into existence.
   * @constructor
   * @extends {HTMLLIElement}
   */
  function NavDot(page, title, titleIsEditable, animate) {
    var dot = cr.doc.createElement('li');
    dot.__proto__ = NavDot.prototype;
    dot.initialize(page, title, titleIsEditable, animate);

    return dot;
  }

  NavDot.prototype = {
    __proto__: HTMLLIElement.prototype,

    initialize: function(page, title, titleIsEditable, animate) {
      this.className = 'dot';
      this.setAttribute('tabindex', 0);
      this.setAttribute('role', 'button');

      this.page_ = page;
      this.title_ = title;
      this.titleIsEditable_ = titleIsEditable;

      var selectionBar = this.ownerDocument.createElement('div');
      selectionBar.className = 'selection-bar';
      this.appendChild(selectionBar);

      // TODO(estade): should there be some limit to the number of characters?
      this.input_ = this.ownerDocument.createElement('input');
      this.input_.setAttribute('spellcheck', false);
      this.input_.value = title;
      // Take the input out of the tab-traversal focus order.
      this.input_.disabled = true;
      this.appendChild(this.input_);

      this.addEventListener('click', this.onClick_);
      this.addEventListener('dblclick', this.onDoubleClick_);
      this.dragWrapper_ = new DragWrapper(this, this);
      this.addEventListener('webkitTransitionEnd', this.onTransitionEnd_);

      this.input_.addEventListener('blur', this.onInputBlur_.bind(this));
      this.input_.addEventListener('mousedown',
                                   this.onInputMouseDown_.bind(this));
      this.input_.addEventListener('keydown', this.onInputKeyDown_.bind(this));

      if (animate) {
        this.classList.add('small');
        var self = this;
        window.setTimeout(function() {
          self.classList.remove('small');
        }, 0);
      }
    },

    /**
     * Gets the associated TilePage.
     * @return {TilePage}
     */
    get page() {
      return this.page_;
    },

    /**
     * Removes the dot from the page after transitioning to 0 width.
     */
    animateRemove: function() {
      this.classList.add('small');
    },

    /**
     * Navigates the card slider to the page for this dot.
     */
    switchToPage: function() {
      ntp4.getCardSlider().selectCardByValue(this.page_, true);
    },

    /**
     * Clicking causes the associated page to show.
     * @param {Event} e The click event.
     * @private
     */
    onClick_: function(e) {
      this.switchToPage();
      // The explicit focus call is necessary because of overriding the default
      // handling in onInputMouseDown_.
      if (this.ownerDocument.activeElement != this.input_)
        this.focus();

      chrome.send('introMessageDismissed');
      e.stopPropagation();
    },

    /**
     * Double clicks allow the user to edit the page title.
     * @param {Event} e The click event.
     * @private
     */
    onDoubleClick_: function(e) {
      if (this.titleIsEditable_) {
        this.input_.disabled = false;
        this.input_.focus();
        this.input_.select();
      }
    },

    /**
     * Prevent mouse down on the input from selecting it.
     * @param {Event} e The click event.
     * @private
     */
    onInputMouseDown_: function(e) {
      if (this.ownerDocument.activeElement != this.input_)
        e.preventDefault();
    },

    /**
     * Handle keypresses on the input.
     * @param {Event} e The click event.
     * @private
     */
    onInputKeyDown_: function(e) {
      switch (e.keyIdentifier) {
        case 'U+001B':  // Escape cancels edits.
          this.input_.value = this.title_;
        case 'Enter':  // Fall through.
          this.input_.blur();
          break;
      }
    },

    /**
     * When the input blurs, commit the edited changes.
     * @param {Event} e The blur event.
     * @private
     */
    onInputBlur_: function(e) {
      window.getSelection().removeAllRanges();
      this.title_ = this.input_.value;
      ntp4.saveAppPageName(this.page_, this.title_);
      this.input_.disabled = true;
    },

    shouldAcceptDrag: function(e) {
      return this.page_.shouldAcceptDrag(e);
    },

    /**
     * A drag has entered the navigation dot. If the user hovers long enough,
     * we will navigate to the relevant page.
     * @param {Event} e The MouseOver event for the drag.
     * @private
     */
    doDragEnter: function(e) {
      var self = this;
      function navPageClearTimeout() {
        self.switchToPage();
        self.dragNavTimeout = null;
      }
      this.dragNavTimeout = window.setTimeout(navPageClearTimeout, 500);

      this.doDragOver(e);
    },

    /**
     * A dragged element has moved over the navigation dot. Show the correct
     * indicator and prevent default handling so the <input> won't act as a drag
     * target.
     * @param {Event} e The MouseOver event for the drag.
     * @private
     */
    doDragOver: function(e) {
      e.preventDefault();

      if (!this.dragWrapper_.isCurrentDragTarget)
        e.dataTransfer.dropEffect = 'none';
      else
        this.page_.setDropEffect(e.dataTransfer);
    },

    /**
     * A dragged element has been dropped on the navigation dot. Tell the page
     * to append it.
     * @param {Event} e The MouseOver event for the drag.
     * @private
     */
    doDrop: function(e) {
      e.stopPropagation();
      var tile = ntp4.getCurrentlyDraggingTile();
      if (tile && tile.tilePage != this.page_)
        this.page_.appendDraggingTile();
      // TODO(estade): handle non-tile drags.

      this.cancelDelayedSwitch_();
    },

    /**
     * The drag has left the navigation dot.
     * @param {Event} e The MouseOver event for the drag.
     * @private
     */
    doDragLeave: function(e) {
      this.cancelDelayedSwitch_();
    },

    /**
     * Cancels the timer for page switching.
     * @private
     */
    cancelDelayedSwitch_: function() {
      if (this.dragNavTimeout) {
        window.clearTimeout(this.dragNavTimeout);
        this.dragNavTimeout = null;
      }
    },

    /**
     * A transition has ended.
     * @param {Event} e The transition end event.
     * @private
     */
    onTransitionEnd_: function(e) {
      if (e.propertyName === 'max-width' && this.classList.contains('small'))
        this.parentNode.removeChild(this);
    },
  };

  return {
    NavDot: NavDot,
  };
});
</script>
<script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview New tab page
 * This is the main code for the new tab page used by touch-enabled Chrome
 * browsers.  For now this is still a prototype.
 */

// Use an anonymous function to enable strict mode just for this file (which
// will be concatenated with other files when embedded in Chrome
cr.define('ntp4', function() {
  'use strict';

  /**
   * The CardSlider object to use for changing app pages.
   * @type {CardSlider|undefined}
   */
  var cardSlider;

  /**
   * The 'page-list' element.
   * @type {!Element|undefined}
   */
  var pageList;

  /**
   * A list of all 'tile-page' elements.
   * @type {!NodeList|undefined}
   */
  var tilePages;

  /**
   * The Most Visited page.
   * @type {!Element|undefined}
   */
  var mostVisitedPage;

  /**
   * A list of all 'apps-page' elements.
   * @type {!NodeList|undefined}
   */
  var appsPages;

  /**
   * The Bookmarks page.
   * @type {!Element|undefined}
   */
  var bookmarksPage;

  /**
   * The 'dots-list' element.
   * @type {!Element|undefined}
   */
  var dotList;

  /**
   * The 'notification-container' element.
   * @type {!Element|undefined}
   */
  var notificationContainer;

  /**
   * The left and right paging buttons.
   * @type {!Element|undefined}
   */
  var pageSwitcherStart;
  var pageSwitcherEnd;

  /**
   * The 'trash' element.  Note that technically this is unnecessary,
   * JavaScript creates the object for us based on the id.  But I don't want
   * to rely on the ID being the same, and JSCompiler doesn't know about it.
   * @type {!Element|undefined}
   */
  var trash;

  /**
   * The type of page that is currently shown. The value is a numerical ID.
   * @type {number}
   */
  var shownPage = 0;

  /**
   * The index of the page that is currently shown, within the page type.
   * For example if the third Apps page is showing, this will be 2.
   * @type {number}
   */
  var shownPageIndex = 0;

  /**
   * EventTracker for managing event listeners for page events.
   * @type {!EventTracker}
   */
  var eventTracker = new EventTracker;

  /**
   * Object for accessing localized strings.
   * @type {!LocalStrings}
   */
  var localStrings = new LocalStrings;

  /**
   * If non-null, this is the ID of the app to highlight to the user the next
   * time getAppsCallback runs. "Highlight" in this case means to switch to
   * the page and run the new tile animation.
   * @type {String}
   */
  var highlightAppId = null;

  /**
   * If non-null, an info bubble for showing messages to the user. It points at
   * the Most Visited label, and is used to draw more attention to the
   * navigation dot UI.
   * @type {!Element|undefined}
   */
  var infoBubble;

  /**
   * The time in milliseconds for most transitions.  This should match what's
   * in new_tab.css.  Unfortunately there's no better way to try to time
   * something to occur until after a transition has completed.
   * @type {number}
   * @const
   */
  var DEFAULT_TRANSITION_TIME = 500;

  /**
   * Invoked at startup once the DOM is available to initialize the app.
   */
  function initialize() {
    cr.enablePlatformSpecificCSSRules();

    // Load the current theme colors.
    themeChanged();

    dotList = getRequiredElement('dot-list');
    pageList = getRequiredElement('page-list');
    trash = getRequiredElement('trash');
    new ntp4.Trash(trash);

    shownPage = templateData['shown_page_type'];
    shownPageIndex = templateData['shown_page_index'];

    // Request data on the apps so we can fill them in.
    // Note that this is kicked off asynchronously.  'getAppsCallback' will be
    // invoked at some point after this function returns.
    chrome.send('getApps');

    // Prevent touch events from triggering any sort of native scrolling
    document.addEventListener('touchmove', function(e) {
      e.preventDefault();
    }, true);

    tilePages = pageList.getElementsByClassName('tile-page');
    appsPages = pageList.getElementsByClassName('apps-page');

    pageSwitcherStart = getRequiredElement('page-switcher-start');
    ntp4.initializePageSwitcher(pageSwitcherStart);
    pageSwitcherEnd = getRequiredElement('page-switcher-end');
    ntp4.initializePageSwitcher(pageSwitcherEnd);

    notificationContainer = getRequiredElement('notification-container');
    notificationContainer.addEventListener(
        'webkitTransitionEnd', onNotificationTransitionEnd);

    // Initialize the cardSlider without any cards at the moment
    var sliderFrame = getRequiredElement('card-slider-frame');
    cardSlider = new CardSlider(sliderFrame, pageList, sliderFrame.offsetWidth);
    cardSlider.initialize();

    // Ensure the slider is resized appropriately with the window
    window.addEventListener('resize', function() {
      cardSlider.resize(sliderFrame.offsetWidth);
      updatePageSwitchers();
    });

    // Handle the page being changed
    pageList.addEventListener(
        CardSlider.EventType.CARD_CHANGED,
        cardChangedHandler);

    cr.ui.decorate($('recently-closed-menu-button'), ntp4.RecentMenuButton);
    chrome.send('getRecentlyClosedTabs');

    mostVisitedPage = new ntp4.MostVisitedPage();
    appendTilePage(mostVisitedPage, localStrings.getString('mostvisited'));
    chrome.send('getMostVisited');

    if (localStrings.getString('ntp4_intro_message')) {
      infoBubble = new cr.ui.Bubble;
      infoBubble.anchorNode = mostVisitedPage.navigationDot;

      var bubbleContent = $('ntp4-intro-bubble-contents');
      infoBubble.content = bubbleContent;
      bubbleContent.hidden = false;

      infoBubble.handleCloseEvent = function() {
        this.hide();
        chrome.send('introMessageDismissed');
      }
      infoBubble.querySelector('a').onclick = infoBubble.hide.bind(infoBubble);

      infoBubble.show();
      chrome.send('introMessageSeen');
    }

    /*
    bookmarksPage = new ntp4.BookmarksPage();
    appendTilePage(bookmarksPage, localStrings.getString('bookmarksPage'));
    chrome.send('getBookmarksData');
    */

    var serverpromo = localStrings.getString('serverpromo');
    if (serverpromo) {
      showNotification(parseHtmlSubset(serverpromo), [], function() {
        chrome.send('closePromo');
      }, 60000);
    }
  }

  /**
   * Simple common assertion API
   * @param {*} condition The condition to test.  Note that this may be used to
   *     test whether a value is defined or not, and we don't want to force a
   *     cast to Boolean.
   * @param {string=} opt_message A message to use in any error.
   */
  function assert(condition, opt_message) {
    'use strict';
    if (!condition) {
      var msg = 'Assertion failed';
      if (opt_message)
        msg = msg + ': ' + opt_message;
      throw new Error(msg);
    }
  }

  /**
   * Get an element that's known to exist by its ID. We use this instead of just
   * calling getElementById and not checking the result because this lets us
   * satisfy the JSCompiler type system.
   * @param {string} id The identifier name.
   * @return {!Element} the Element.
   */
  function getRequiredElement(id) {
    var element = document.getElementById(id);
    assert(element, 'Missing required element: ' + id);
    return element;
  }

  /**
   * Callback invoked by chrome with the apps available.
   *
   * Note that calls to this function can occur at any time, not just in
   * response to a getApps request. For example, when a user installs/uninstalls
   * an app on another synchronized devices.
   * @param {Object} data An object with all the data on available
   *        applications.
   */
  function getAppsCallback(data) {
    var startTime = Date.now();

    // Clear any existing apps pages and dots.
    // TODO(rbyers): It might be nice to preserve animation of dots after an
    // uninstall. Could we re-use the existing page and dot elements?  It seems
    // unfortunate to have Chrome send us the entire apps list after an
    // uninstall.
    while (appsPages.length > 0) {
      var page = appsPages[0];
      var dot = page.navigationDot;

      eventTracker.remove(page);
      page.tearDown();
      page.parentNode.removeChild(page);
      dot.parentNode.removeChild(dot);
    }

    // Get the array of apps and add any special synthesized entries
    var apps = data.apps;

    // Get a list of page names
    var pageNames = data.appPageNames;

    function stringListIsEmpty(list) {
      for (var i = 0; i < list.length; i++) {
        if (list[i])
          return false;
      }
      return true;
    }

    if (!pageNames || stringListIsEmpty(pageNames))
      pageNames = [localStrings.getString('appDefaultPageName')];

    // Sort by launch index
    apps.sort(function(a, b) {
      return a.app_launch_index - b.app_launch_index;
    });

    // An app to animate (in case it was just installed).
    var highlightApp;

    // Add the apps, creating pages as necessary
    for (var i = 0; i < apps.length; i++) {
      var app = apps[i];
      var pageIndex = (app.page_index || 0);
      while (pageIndex >= appsPages.length) {
        var pageName = '';
        if (appsPages.length < pageNames.length)
          pageName = pageNames[appsPages.length];

        var origPageCount = appsPages.length;
        appendTilePage(new ntp4.AppsPage(), pageName, bookmarksPage);
        // Confirm that appsPages is a live object, updated when a new page is
        // added (otherwise we'd have an infinite loop)
        assert(appsPages.length == origPageCount + 1, 'expected new page');
      }

      if (app.id == highlightAppId)
        highlightApp = app;
      else
        appsPages[pageIndex].appendApp(app);
    }

    ntp4.AppsPage.setPromo(data.showPromo ? data : null);

    // Tell the slider about the pages.
    updateSliderCards();

    if (highlightApp)
      appAdded(highlightApp, true);

    // Mark the current page.
    cardSlider.currentCardValue.navigationDot.classList.add('selected');
    logEvent('apps.layout: ' + (Date.now() - startTime));

    document.documentElement.classList.remove('starting-up');
  }

  /**
   * Called by chrome when a new app has been added to chrome or has been
   * enabled if previously disabled.
   * @param {Object} appData A data structure full of relevant information for
   *     the app.
   */
  function appAdded(appData, opt_highlight) {
    if (appData.id == highlightAppId) {
      opt_highlight = true;
      highlightAppId = null;
    }

    var pageIndex = appData.page_index || 0;

    if (pageIndex >= appsPages.length) {
      while (pageIndex >= appsPages.length) {
        appendTilePage(new ntp4.AppsPage(), '', bookmarksPage);
      }
      updateSliderCards();
    }

    var page = appsPages[pageIndex];
    var app = $(appData.id);
    if (app)
      app.replaceAppData(appData);
    else
      page.appendApp(appData, opt_highlight);
  }

  /**
   * Sets that an app should be highlighted if it is added. Called right before
   * appAdded for new installs.
   */
  function setAppToBeHighlighted(appId) {
    highlightAppId = appId;
  }

  /**
   * Called by chrome when an existing app has been disabled or
   * removed/uninstalled from chrome.
   * @param {Object} appData A data structure full of relevant information for
   *     the app.
   * @param {boolean} isUninstall True if the app is being uninstalled;
   *     false if the app is being disabled.
   */
  function appRemoved(appData, isUninstall) {
    var app = $(appData.id);
    assert(app, 'trying to remove an app that doesn\'t exist');

    if (!isUninstall)
      app.replaceAppData(appData);
    else
      app.remove();
  }

  /**
   * Given a theme resource name, construct a URL for it.
   * @param {string} resourceName The name of the resource.
   * @return {string} A url which can be used to load the resource.
   */
  function getThemeUrl(resourceName) {
    return 'chrome://theme/' + resourceName;
  }

  /**
   * Callback invoked by chrome whenever an app preference changes.
   * @param {Object} data An object with all the data on available
   *     applications.
   */
  function appsPrefChangeCallback(data) {
    for (var i = 0; i < data.apps.length; ++i) {
      $(data.apps[i].id).appData = data.apps[i];
    }
  }

  /**
   * Listener for offline status change events. Updates apps that are
   * not offline-enabled to be grayscale if the browser is offline.
   */
  function updateOfflineEnabledApps() {
    var apps = document.querySelectorAll('.app');
    for (var i = 0; i < apps.length; ++i) {
      if (apps[i].appData.enabled && !apps[i].appData.offline_enabled) {
        apps[i].setIcon();
        apps[i].loadIcon();
      }
    }
  }

  function getCardSlider() {
    return cardSlider;
  }

  /**
   * Invoked whenever the pages in apps-page-list have changed so that
   * the Slider knows about the new elements.
   */
  function updateSliderCards() {
    var pageNo = Math.min(cardSlider.currentCard, tilePages.length - 1);
    cardSlider.setCards(Array.prototype.slice.call(tilePages), pageNo);
    switch (shownPage) {
      case templateData['bookmarks_page_id']:
      case templateData['apps_page_id']:
        cardSlider.selectCardByValue(
            appsPages[Math.min(shownPageIndex, appsPages.length - 1)]);
        break;
/*
        cardSlider.selectCardByValue(bookmarksPage);
        break;
*/
      case templateData['most_visited_page_id']:
        cardSlider.selectCardByValue(mostVisitedPage);
        break;
    }
  }

  /**
   * Appends a tile page (for bookmarks or most visited).
   *
   * @param {TilePage} page The page element.
   * @param {string} title The title of the tile page.
   * @param {TilePage} refNode Optional reference node to insert in front of.
   * When refNode is falsey, |page| will just be appended to the end of the
   * page list.
   */
  function appendTilePage(page, title, refNode) {
    // When refNode is falsey, insertBefore acts just like appendChild.
    pageList.insertBefore(page, refNode);

    // If we're appending an AppsPage and it's a temporary page, animate it.
    var animate = page instanceof ntp4.AppsPage &&
                  page.classList.contains('temporary');
    // Make a deep copy of the dot template to add a new one.
    var newDot = new ntp4.NavDot(page, title, true, animate);
    page.navigationDot = newDot;
    dotList.insertBefore(newDot, refNode ? refNode.navigationDot : null);

    if (infoBubble)
      window.setTimeout(infoBubble.reposition.bind(infoBubble), 0);

    eventTracker.add(page, 'pagelayout', onPageLayout);
  }

  /**
   * Search an elements ancestor chain for the nearest element that is a member
   * of the specified class.
   * @param {!Element} element The element to start searching from.
   * @param {string} className The name of the class to locate.
   * @return {Element} The first ancestor of the specified class or null.
   */
  function getParentByClassName(element, className) {
    for (var e = element; e; e = e.parentElement) {
      if (e.classList.contains(className))
        return e;
    }
    return null;
  }

  /**
   * Called whenever tiles should be re-arranging themselves out of the way of a
   * moving or insert tile.
   */
  function enterRearrangeMode() {
    var tempPage = new ntp4.AppsPage();
    tempPage.classList.add('temporary');
    appendTilePage(tempPage, '', bookmarksPage);
    var tempIndex = Array.prototype.indexOf.call(tilePages, tempPage);
    if (cardSlider.currentCard >= tempIndex)
      cardSlider.currentCard += 1;
    updateSliderCards();

    if (ntp4.getCurrentlyDraggingTile().firstChild.canBeRemoved())
      $('footer').classList.add('showing-trash-mode');
  }

  /**
   * Invoked whenever some app is released
   * @param {Grabber.Event} e The Grabber RELEASE event.
   */
  function leaveRearrangeMode(e) {
    var tempPage = document.querySelector('.tile-page.temporary');
    var dot = tempPage.navigationDot;
    if (!tempPage.tileCount && tempPage != cardSlider.currentCardValue) {
      dot.animateRemove();
      var tempIndex = Array.prototype.indexOf.call(tilePages, tempPage);
      if (cardSlider.currentCard > tempIndex)
        cardSlider.currentCard -= 1;
      tempPage.parentNode.removeChild(tempPage);
      updateSliderCards();
    } else {
      tempPage.classList.remove('temporary');
      saveAppPageName(tempPage, '');
    }

    $('footer').classList.remove('showing-trash-mode');
  }

  /**
   * Callback for the 'click' event on a page switcher.
   * @param {Event} e The event.
   */
  function onPageSwitcherClicked(e) {
    cardSlider.selectCard(cardSlider.currentCard +
        (e.currentTarget == pageSwitcherStart ? -1 : 1), true);
  }

  /**
   * Handler for the mousewheel event on a pager. We pass through the scroll
   * to the page.
   * @param {Event} e The mousewheel event.
   */
  function onPageSwitcherScrolled(e) {
    cardSlider.currentCardValue.scrollBy(-e.wheelDeltaY);
  };

  /**
   * Callback for the 'pagelayout' event.
   * @param {Event} e The event.
   */
  function onPageLayout(e) {
    if (Array.prototype.indexOf.call(tilePages, e.currentTarget) !=
        cardSlider.currentCard) {
      return;
    }

    updatePageSwitchers();
  }

  /**
   * Adjusts the size and position of the page switchers according to the
   * layout of the current card.
   */
  function updatePageSwitchers() {
    var page = cardSlider.currentCardValue;

    pageSwitcherStart.hidden = !page || (cardSlider.currentCard == 0);
    pageSwitcherEnd.hidden = !page ||
        (cardSlider.currentCard == cardSlider.cardCount - 1);

    if (!page)
      return;

    var pageSwitcherLeft = isRTL() ? pageSwitcherEnd : pageSwitcherStart;
    var pageSwitcherRight = isRTL() ? pageSwitcherStart : pageSwitcherEnd;
    var scrollbarWidth = page.scrollbarWidth;
    pageSwitcherLeft.style.width =
        (page.sideMargin + 13) + 'px';
    pageSwitcherLeft.style.left = '0';
    pageSwitcherRight.style.width =
        (page.sideMargin - scrollbarWidth + 13) + 'px';
    pageSwitcherRight.style.right = scrollbarWidth + 'px';

    var offsetTop = page.querySelector('.tile-page-content').offsetTop + 'px';
    pageSwitcherLeft.style.top = offsetTop;
    pageSwitcherRight.style.top = offsetTop;
    pageSwitcherLeft.style.paddingBottom = offsetTop;
    pageSwitcherRight.style.paddingBottom = offsetTop;
  }

  /**
   * Returns the index of the given page.
   * @param {AppsPage} page The AppsPage for we wish to find.
   * @return {number} The index of |page|, or -1 if it is not here.
   */
  function getAppsPageIndex(page) {
    return Array.prototype.indexOf.call(appsPages, page);
  }

  // TODO(estade): rename newtab.css to new_tab_theme.css
  function themeChanged(hasAttribution) {
    $('themecss').href = 'chrome://theme/css/newtab.css?' + Date.now();
    if (typeof hasAttribution != 'undefined')
      document.documentElement.setAttribute('hasattribution', hasAttribution);
    updateLogo();
    updateAttribution();
  }

  /**
   * Sets the proper image for the logo at the bottom left.
   */
  function updateLogo() {
    var imageId = 'IDR_PRODUCT_LOGO';
    if (document.documentElement.getAttribute('customlogo') == 'true')
      imageId = 'IDR_CUSTOM_PRODUCT_LOGO';

    $('logo-img').src = 'chrome://theme/' + imageId + '?' + Date.now();
  }

  /**
   * Attributes the attribution image at the bottom left.
   */
  function updateAttribution() {
    var attribution = $('attribution');
    if (document.documentElement.getAttribute('hasattribution') == 'true') {
      $('attribution-img').src =
          'chrome://theme/IDR_THEME_NTP_ATTRIBUTION?' + Date.now();
      attribution.hidden = false;
    } else {
      attribution.hidden = true;
    }
  }

  /**
   * Handler for CARD_CHANGED on cardSlider.
   * @param {Event} e The CARD_CHANGED event.
   */
  function cardChangedHandler(e) {
    var page = e.cardSlider.currentCardValue;

    // Don't change shownPage until startup is done (and page changes actually
    // reflect user actions).
    if (!document.documentElement.classList.contains('starting-up')) {
      if (page.classList.contains('apps-page')) {
        shownPage = templateData['apps_page_id'];
        shownPageIndex = getAppsPageIndex(page);
      } else if (page.classList.contains('most-visited-page')) {
        shownPage = templateData['most_visited_page_id'];
        shownPageIndex = 0;
      } else if (page.classList.contains('bookmarks-page')) {
        shownPage = templateData['bookmarks_page_id'];
        shownPageIndex = 0;
      } else {
        console.error('unknown page selected');
      }
      chrome.send('pageSelected', [shownPage, shownPageIndex]);
    }

    // Update the active dot
    var curDot = dotList.getElementsByClassName('selected')[0];
    if (curDot)
      curDot.classList.remove('selected');
    page.navigationDot.classList.add('selected');
    updatePageSwitchers();
  }

  /**
   * Timeout ID.
   * @type {number}
   */
  var notificationTimeout_ = 0;

  /**
   * Shows the notification bubble.
   * @param {string|Node} message The notification message or node to use as
   *     message.
   * @param {Array.<{text: string, action: function()}>} links An array of
   *     records describing the links in the notification. Each record should
   *     have a 'text' attribute (the display string) and an 'action' attribute
   *     (a function to run when the link is activated).
   * @param {Function} opt_closeHandler The callback invoked if the user
   *     manually dismisses the notification.
   */
  function showNotification(message, links, opt_closeHandler, opt_timeout) {
    window.clearTimeout(notificationTimeout_);

    var span = document.querySelector('#notification > span');
    if (typeof message == 'string') {
      span.textContent = message;
    } else {
      span.textContent = '';  // Remove all children.
      span.appendChild(message);
    }

    var linksBin = $('notificationLinks');
    linksBin.textContent = '';
    for (var i = 0; i < links.length; i++) {
      var link = linksBin.ownerDocument.createElement('div');
      link.textContent = links[i].text;
      var action = links[i].action;
      link.onclick = function(e) {
        action();
        hideNotification();
      }
      link.setAttribute('role', 'button');
      link.setAttribute('tabindex', 0);
      link.className = "linkButton";
      linksBin.appendChild(link);
    }

    document.querySelector('#notification button').onclick = function(e) {
      if (opt_closeHandler)
        opt_closeHandler();
      hideNotification();
    };

    var timeout = opt_timeout || 10000;
    notificationContainer.hidden = false;
    notificationContainer.classList.remove('inactive');
    notificationTimeout_ = window.setTimeout(hideNotification, timeout);
  }

  /**
   * Hide the notification bubble.
   */
  function hideNotification() {
    notificationContainer.classList.add('inactive');
  }

  /**
   * When done fading out, set hidden to true so the notification can't be
   * tabbed to or clicked.
   */
  function onNotificationTransitionEnd(e) {
    if (notificationContainer.classList.contains('inactive'));
      notificationContainer.hidden = true;
  }

  function setRecentlyClosedTabs(dataItems) {
    $('recently-closed-menu-button').dataItems = dataItems;
  }

  function setMostVisitedPages(data, hasBlacklistedUrls) {
    mostVisitedPage.data = data;
  }

  function setBookmarksData(data) {
    bookmarksPage.data = data;
  }

  /**
   * Check the directionality of the page.
   * @return {boolean} True if Chrome is running an RTL UI.
   */
  function isRTL() {
    return document.documentElement.dir == 'rtl';
  }

  /*
   * Save the name of an app page.
   * Store the app page name into the preferences store.
   * @param {AppsPage} appPage The app page for which we wish to save.
   * @param {string} name The name of the page.
   */
  function saveAppPageName(appPage, name) {
    var index = getAppsPageIndex(appPage);
    assert(index != -1);
    chrome.send('saveAppPageName', [name, index]);
  }

  function bookmarkImportBegan() {
    bookmarksPage.bookmarkImportBegan.apply(bookmarksPage, arguments);
  }

  function bookmarkImportEnded() {
    bookmarksPage.bookmarkImportEnded.apply(bookmarksPage, arguments);
  }

  function bookmarkNodeAdded() {
    bookmarksPage.bookmarkNodeAdded.apply(bookmarksPage, arguments);
  }

  function bookmarkNodeChanged() {
    bookmarksPage.bookmarkNodeChanged.apply(bookmarksPage, arguments);
  }

  function bookmarkNodeChildrenReordered() {
    bookmarksPage.bookmarkNodeChildrenReordered.apply(bookmarksPage, arguments);
  }

  function bookmarkNodeMoved() {
    bookmarksPage.bookmarkNodeMoved.apply(bookmarksPage, arguments);
  }

  function bookmarkNodeRemoved() {
    bookmarksPage.bookmarkNodeRemoved.apply(bookmarksPage, arguments);
  }

  /**
   * Set the dominant color for a node. This will be called in response to
   * getFaviconDominantColor. The node represented by |id| better have a setter
   * for stripeColor.
   * @param {string} id The ID of a node.
   * @param {string} color The color represented as a CSS string.
   */
  function setStripeColor(id, color) {
    var node = $(id);
    if (node)
      node.stripeColor = color;
  };

  // Return an object with all the exports
  return {
    appAdded: appAdded,
    appRemoved: appRemoved,
    appsPrefChangeCallback: appsPrefChangeCallback,
    assert: assert,
    bookmarkImportBegan: bookmarkImportBegan,
    bookmarkImportEnded: bookmarkImportEnded,
    bookmarkNodeAdded: bookmarkNodeAdded,
    bookmarkNodeChanged: bookmarkNodeChanged,
    bookmarkNodeChildrenReordered: bookmarkNodeChildrenReordered,
    bookmarkNodeMoved: bookmarkNodeMoved,
    bookmarkNodeRemoved: bookmarkNodeRemoved,
    enterRearrangeMode: enterRearrangeMode,
    getAppsCallback: getAppsCallback,
    getAppsPageIndex: getAppsPageIndex,
    getCardSlider: getCardSlider,
    initialize: initialize,
    isRTL: isRTL,
    leaveRearrangeMode: leaveRearrangeMode,
    saveAppPageName: saveAppPageName,
    setAppToBeHighlighted: setAppToBeHighlighted,
    setBookmarksData: setBookmarksData,
    setMostVisitedPages: setMostVisitedPages,
    setRecentlyClosedTabs: setRecentlyClosedTabs,
    setStripeColor: setStripeColor,
    showNotification: showNotification,
    themeChanged: themeChanged,
    updateOfflineEnabledApps: updateOfflineEnabledApps
  };
});

// publish ntp globals
// TODO(estade): update the content handlers to use ntp namespace instead of
// making these global.
var assert = ntp4.assert;
var getAppsCallback = ntp4.getAppsCallback;
var appsPrefChangeCallback = ntp4.appsPrefChangeCallback;
var themeChanged = ntp4.themeChanged;
var recentlyClosedTabs = ntp4.setRecentlyClosedTabs;
var setMostVisitedPages = ntp4.setMostVisitedPages;

document.addEventListener('DOMContentLoaded', ntp4.initialize);
window.addEventListener('online', ntp4.updateOfflineEnabledApps);
window.addEventListener('offline', ntp4.updateOfflineEnabledApps);
</script>
<script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview The recently closed menu: button, model data, and menu.
 */

cr.define('ntp4', function() {
  'use strict';

  var localStrings = new LocalStrings();

  /**
   * Returns the text used for a recently closed window.
   * @param {number} numTabs Number of tabs in the window.
   * @return {string} The text to use.
   */
  function formatTabsText(numTabs) {
    if (numTabs == 1)
      return localStrings.getString('closedwindowsingle');
    return localStrings.getStringF('closedwindowmultiple', numTabs);
  }

  var Menu = cr.ui.Menu;
  var MenuItem = cr.ui.MenuItem;
  var MenuButton = cr.ui.MenuButton;
  var RecentMenuButton = cr.ui.define('button');

  RecentMenuButton.prototype = {
    __proto__: MenuButton.prototype,

    decorate: function() {
      MenuButton.prototype.decorate.call(this);
      this.menu = new Menu;
      cr.ui.decorate(this.menu, Menu);
      this.menu.classList.add('recent-menu');
      document.body.appendChild(this.menu);

      this.needsRebuild_ = true;
      this.classList.add('invisible');
      this.anchorType = cr.ui.AnchorType.ABOVE;
      this.invertLeftRight = true;
    },

    /**
     * Shows the menu, first rebuilding it if necessary.
     * TODO(estade): the right of the menu should align with the right of the
     * button.
     * @override
     */
    showMenu: function() {
      if (this.needsRebuild_) {
        this.menu.textContent = '';
        this.dataItems_.forEach(this.addItem_, this);
        this.needsRebuild_ = false;
      }

      MenuButton.prototype.showMenu.call(this);
    },

    /**
     * Sets the menu model data.
     * @param {Array} dataItems Array of objects that describe the apps.
     */
    set dataItems(dataItems) {
      this.dataItems_ = dataItems;
      this.needsRebuild_ = true;
      if (dataItems.length)
        this.classList.remove('invisible');
      else
        this.classList.add('invisible');
    },

    /**
     * Adds an app to the menu.
     * @param {Object} data An object encapsulating all data about the app.
     * @private
     */
    addItem_: function(data) {
      var isWindow = data.type == 'window';
      var a = this.ownerDocument.createElement('a');
      a.className = 'recent-menu-item';
      if (isWindow) {
        a.href = '';
        a.classList.add('recent-window');
        a.textContent = formatTabsText(data.tabs.length);
      } else {
        a.href = data.url;
        a.style.backgroundImage = 'url(chrome://favicon/' + data.url + ')';
        a.textContent = data.title;
        // TODO(estade): add app ping url.
      }

      function onActivate(e) {
        chrome.send('recordAppLaunchByURL',
                    [encodeURIComponent(data.url),
                     ntp4.APP_LAUNCH.NTP_RECENTLY_CLOSED]);
        // TODO(estade): don't convert to string.
        chrome.send('reopenTab', [String(data.sessionId)]);
        // We are likely deleted by this point!

        e.preventDefault();
      }
      a.addEventListener('activate', onActivate);

      this.menu.appendChild(a);
      cr.ui.decorate(a, MenuItem);
    },
  };

  return {
    RecentMenuButton: RecentMenuButton,
  };
});
</script>
<script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview Touch Handler. Class that handles all touch events and
 * uses them to interpret higher level gestures and behaviors. TouchEvent is a
 * built in mobile safari type:
 * http://developer.apple.com/safari/library/documentation/UserExperience/Reference/TouchEventClassReference/TouchEvent/TouchEvent.html.
 * This class is intended to work with all webkit browsers, tested on Chrome and
 * iOS.
 *
 * The following types of gestures are currently supported.  See the definition
 * of TouchHandler.EventType for details.
 *
 * Single Touch:
 *      This provides simple single-touch events.  Any secondary touch is
 *      ignored.
 *
 * Drag:
 *      A single touch followed by some movement. This behavior will handle all
 *      of the required events and report the properties of the drag to you
 *      while the touch is happening and at the end of the drag sequence. This
 *      behavior will NOT perform the actual dragging (redrawing the element)
 *      for you, this responsibility is left to the client code.
 *
 * Long press:
 *     When your element is touched and held without any drag occuring, the
 *     LONG_PRESS event will fire.
 */

// Use an anonymous function to enable strict mode just for this file (which
// will be concatenated with other files when embedded in Chrome)
var TouchHandler = (function() {
  'use strict';

  /**
   * A TouchHandler attaches to an Element, listents for low-level touch (or
   * mouse) events and dispatching higher-level events on the element.
   * @param {!Element} element The element to listen on and fire events
   * for.
   * @constructor
   */
  function TouchHandler(element) {
    /**
     * @type {!Element}
     * @private
     */
    this.element_ = element;

    /**
     * The absolute sum of all touch y deltas.
     * @type {number}
     * @private
     */
    this.totalMoveY_ = 0;

    /**
     * The absolute sum of all touch x deltas.
     * @type {number}
     * @private
     */
    this.totalMoveX_ = 0;

    /**
     * An array of tuples where the first item is the horizontal component of a
     * recent relevant touch and the second item is the touch's time stamp. Old
     * touches are removed based on the max tracking time and when direction
     * changes.
      * @type {!Array.<number>}
      * @private
      */
    this.recentTouchesX_ = [];

    /**
     * An array of tuples where the first item is the vertical component of a
     * recent relevant touch and the second item is the touch's time stamp. Old
     * touches are removed based on the max tracking time and when direction
     * changes.
     * @type {!Array.<number>}
     * @private
     */
    this.recentTouchesY_ = [];

    /**
     * Used to keep track of all events we subscribe to so we can easily clean
     * up
     * @type {EventTracker}
     * @private
     */
    this.events_ = new EventTracker();
  }


  /**
   * DOM Events that may be fired by the TouchHandler at the element
   */
  TouchHandler.EventType = {
    // Fired whenever the element is touched as the only touch to the device.
    // enableDrag defaults to false, set to true to permit dragging.
    TOUCH_START: 'touchHandler:touch_start',

    // Fired when an element is held for a period of time.  Prevents dragging
    // from occuring (even if enableDrag was set to true).
    LONG_PRESS: 'touchHandler:long_press',

    // If enableDrag was set to true at TOUCH_START, DRAG_START will fire when
    // the touch first moves sufficient distance.  enableDrag is set to true but
    // can be reset to false to cancel the drag.
    DRAG_START: 'touchHandler:drag_start',

    // If enableDrag was true after DRAG_START, DRAG_MOVE will fire whenever the
    // touch is moved.
    DRAG_MOVE: 'touchHandler:drag_move',

    // Fired just before TOUCH_END when a drag is released.  Correlates 1:1 with
    // a DRAG_START.
    DRAG_END: 'touchHandler:drag_end',

    // Fired whenever a touch that is being tracked has been released.
    // Correlates 1:1 with a TOUCH_START.
    TOUCH_END: 'touchHandler:touch_end'
  };


  /**
   * The type of event sent by TouchHandler
   * @constructor
   * @param {string} type The type of event (one of Grabber.EventType).
   * @param {boolean} bubbles Whether or not the event should bubble.
   * @param {number} clientX The X location of the touch.
   * @param {number} clientY The Y location of the touch.
   * @param {!Element} touchedElement The element at the current location of the
   *        touch.
   */
  TouchHandler.Event = function(type, bubbles, clientX, clientY,
      touchedElement) {
    var event = document.createEvent('Event');
    event.initEvent(type, bubbles, true);
    event.__proto__ = TouchHandler.Event.prototype;

    /**
     * The X location of the touch affected
     * @type {number}
     */
    event.clientX = clientX;

    /**
     * The Y location of the touch affected
     * @type {number}
     */
    event.clientY = clientY;

    /**
     * The element at the current location of the touch.
     * @type {!Element}
     */
    event.touchedElement = touchedElement;

    return event;
  };

  TouchHandler.Event.prototype = {
    __proto__: Event.prototype,

    /**
     * For TOUCH_START and DRAG START events, set to true to enable dragging or
     * false to disable dragging.
     * @type {boolean|undefined}
     */
    enableDrag: undefined,

    /**
     * For DRAG events, provides the horizontal component of the
     * drag delta. Drag delta is defined as the delta of the start touch
     * position and the current drag position.
     * @type {number|undefined}
     */
    dragDeltaX: undefined,

    /**
     * For DRAG events, provides the vertical component of the
     * drag delta.
     * @type {number|undefined}
     */
    dragDeltaY: undefined
  };

  /**
   * Minimum movement of touch required to be considered a drag.
   * @type {number}
   * @private
   */
  TouchHandler.MIN_TRACKING_FOR_DRAG_ = 8;


  /**
   * The maximum number of ms to track a touch event. After an event is older
   * than this value, it will be ignored in velocity calculations.
   * @type {number}
   * @private
   */
  TouchHandler.MAX_TRACKING_TIME_ = 250;


  /**
   * The maximum number of touches to track.
   * @type {number}
   * @private
   */
  TouchHandler.MAX_TRACKING_TOUCHES_ = 5;


  /**
   * The maximum velocity to return, in pixels per millisecond, that is used
   * to guard against errors in calculating end velocity of a drag. This is a
   * very fast drag velocity.
   * @type {number}
   * @private
   */
  TouchHandler.MAXIMUM_VELOCITY_ = 5;


  /**
   * The velocity to return, in pixel per millisecond, when the time stamps on
   * the events are erroneous. The browser can return bad time stamps if the
   * thread is blocked for the duration of the drag. This is a low velocity to
   * prevent the content from moving quickly after a slow drag. It is less
   * jarring if the content moves slowly after a fast drag.
   * @type {number}
   * @private
   */
  TouchHandler.VELOCITY_FOR_INCORRECT_EVENTS_ = 1;

  /**
   * The time, in milliseconds, that a touch must be held to be considered
   * 'long'.
   * @type {number}
   * @private
   */
  TouchHandler.TIME_FOR_LONG_PRESS_ = 500;

  TouchHandler.prototype = {
    /**
     * If defined, the identifer of the single touch that is active.  Note that
     * 0 is a valid touch identifier - it should not be treated equivalently to
     * undefined.
     * @type {number|undefined}
     * @private
     */
    activeTouch_: undefined,

    /**
     * @type {boolean|undefined}
     * @private
     */
    tracking_: undefined,

    /**
     * @type {number|undefined}
     * @private
     */
    startTouchX_: undefined,

    /**
     * @type {number|undefined}
     * @private
     */
    startTouchY_: undefined,

    /**
     * @type {number|undefined}
     * @private
     */
    endTouchX_: undefined,

    /**
     * @type {number|undefined}
     * @private
     */
    endTouchY_: undefined,

    /**
     * Time of the touchstart event.
     * @type {number|undefined}
     * @private
     */
    startTime_: undefined,

    /**
     * The time of the touchend event.
     * @type {number|undefined}
     * @private
     */
    endTime_: undefined,

    /**
     * @type {number|undefined}
     * @private
     */
    lastTouchX_: undefined,

    /**
     * @type {number|undefined}
     * @private
     */
    lastTouchY_: undefined,

    /**
     * @type {number|undefined}
     * @private
     */
    lastMoveX_: undefined,

    /**
     * @type {number|undefined}
     * @private
     */
    lastMoveY_: undefined,

    /**
     * @type {number|undefined}
     * @private
     */
    longPressTimeout_: undefined,

    /**
     * If defined and true, the next click event should be swallowed
     * @type {boolean|undefined}
     * @private
     */
    swallowNextClick_: undefined,

    /**
     * Start listenting for events.
     * @param {boolean=} opt_capture True if the TouchHandler should listen to
     *      during the capture phase.
     */
    enable: function(opt_capture) {
      var capture = !!opt_capture;

      // Just listen to start events for now. When a touch is occuring we'll
      // want to be subscribed to move and end events on the document, but we
      // don't want to incur the cost of lots of no-op handlers on the document.
      this.events_.add(this.element_, 'touchstart', this.onStart_.bind(this),
                       capture);
      this.events_.add(this.element_, 'mousedown',
                       this.mouseToTouchCallback_(this.onStart_.bind(this)),
                       capture);

      // If the element is long-pressed, we may need to swallow a click
      this.events_.add(this.element_, 'click', this.onClick_.bind(this), true);
    },

    /**
     * Stop listening to all events.
     */
    disable: function() {
      this.stopTouching_();
      this.events_.removeAll();
    },

    /**
     * Wraps a callback with translations of mouse events to touch events.
     * NOTE: These types really should be function(Event) but then we couldn't
     * use this with bind (which operates on any type of function).  Doesn't
     * JSDoc support some sort of polymorphic types?
     * @param {Function} callback The event callback.
     * @return {Function} The wrapping callback.
     * @private
     */
    mouseToTouchCallback_: function(callback) {
      return function(e) {
        // Note that there may be synthesizes mouse events caused by touch
        // events (a mouseDown after a touch-click).  We leave it up to the
        // client to worry about this if it matters to them (typically a short
        // mouseDown/mouseUp without a click is no big problem and it's not
        // obvious how we identify such synthesized events in a general way).
        var touch = {
          // any fixed value will do for the identifier - there will only
          // ever be a single active 'touch' when using the mouse.
          identifier: 0,
          clientX: e.clientX,
          clientY: e.clientY,
          target: e.target
        };
        e.touches = [];
        e.targetTouches = [];
        e.changedTouches = [touch];
        if (e.type != 'mouseup') {
          e.touches[0] = touch;
          e.targetTouches[0] = touch;
        }
        callback(e);
      };
    },

    /**
     * Begin tracking the touchable element, it is eligible for dragging.
     * @private
     */
    beginTracking_: function() {
      this.tracking_ = true;
    },

    /**
     * Stop tracking the touchable element, it is no longer dragging.
     * @private
     */
    endTracking_: function() {
      this.tracking_ = false;
      this.dragging_ = false;
      this.totalMoveY_ = 0;
      this.totalMoveX_ = 0;
    },

    /**
     * Reset the touchable element as if we never saw the touchStart
     * Doesn't dispatch any end events - be careful of existing listeners.
     */
    cancelTouch: function() {
      this.stopTouching_();
      this.endTracking_();
      // If clients needed to be aware of this, we could fire a cancel event
      // here.
    },

    /**
     * Record that touching has stopped
     * @private
     */
    stopTouching_: function() {
      // Mark as no longer being touched
      this.activeTouch_ = undefined;

      // If we're waiting for a long press, stop
      window.clearTimeout(this.longPressTimeout_);

      // Stop listening for move/end events until there's another touch.
      // We don't want to leave handlers piled up on the document.
      // Note that there's no harm in removing handlers that weren't added, so
      // rather than track whether we're using mouse or touch we do both.
      this.events_.remove(document, 'touchmove');
      this.events_.remove(document, 'touchend');
      this.events_.remove(document, 'touchcancel');
      this.events_.remove(document, 'mousemove');
      this.events_.remove(document, 'mouseup');
    },

    /**
     * Touch start handler.
     * @param {!TouchEvent} e The touchstart event.
     * @private
     */
    onStart_: function(e) {
      // Only process single touches.  If there is already a touch happening, or
      // two simultaneous touches then just ignore them.
      if (e.touches.length > 1)
        // Note that we could cancel an active touch here.  That would make
        // simultaneous touch behave similar to near-simultaneous. However, if
        // the user is dragging something, an accidental second touch could be
        // quite disruptive if it cancelled their drag.  Better to just ignore
        // it.
        return;

      // It's still possible there could be an active "touch" if the user is
      // simultaneously using a mouse and a touch input.
      if (this.activeTouch_ !== undefined)
        return;

      var touch = e.targetTouches[0];
      this.activeTouch_ = touch.identifier;

      // We've just started touching so shouldn't swallow any upcoming click
      if (this.swallowNextClick_)
        this.swallowNextClick_ = false;

      // Sign up for end/cancel notifications for this touch.
      // Note that we do this on the document so that even if the user drags
      // their finger off the element, we'll still know what they're doing.
      if (e.type == 'mousedown') {
        this.events_.add(document, 'mouseup',
            this.mouseToTouchCallback_(this.onEnd_.bind(this)), false);
      } else {
        this.events_.add(document, 'touchend', this.onEnd_.bind(this), false);
        this.events_.add(document, 'touchcancel', this.onEnd_.bind(this),
            false);
      }

      // This timeout is cleared on touchEnd and onDrag
      // If we invoke the function then we have a real long press
      window.clearTimeout(this.longPressTimeout_);
      this.longPressTimeout_ = window.setTimeout(
          this.onLongPress_.bind(this),
          TouchHandler.TIME_FOR_LONG_PRESS_);

      // Dispatch the TOUCH_START event
      if (!this.dispatchEvent_(TouchHandler.EventType.TOUCH_START, touch))
        // Dragging was not enabled, nothing more to do
        return;

      // We want dragging notifications
      if (e.type == 'mousedown') {
        this.events_.add(document, 'mousemove',
            this.mouseToTouchCallback_(this.onMove_.bind(this)), false);
      } else {
        this.events_.add(document, 'touchmove', this.onMove_.bind(this), false);
      }

      this.startTouchX_ = this.lastTouchX_ = touch.clientX;
      this.startTouchY_ = this.lastTouchY_ = touch.clientY;
      this.startTime_ = e.timeStamp;

      this.recentTouchesX_ = [];
      this.recentTouchesY_ = [];
      this.recentTouchesX_.push(touch.clientX, e.timeStamp);
      this.recentTouchesY_.push(touch.clientY, e.timeStamp);

      this.beginTracking_();
    },

    /**
     * Given a list of Touches, find the one matching our activeTouch
     * identifier. Note that Chrome currently always uses 0 as the identifier.
     * In that case we'll end up always choosing the first element in the list.
     * @param {TouchList} touches The list of Touch objects to search.
     * @return {!Touch|undefined} The touch matching our active ID if any.
     * @private
     */
    findActiveTouch_: function(touches) {
      assert(this.activeTouch_ !== undefined, 'Expecting an active touch');
      // A TouchList isn't actually an array, so we shouldn't use
      // Array.prototype.filter/some, etc.
      for (var i = 0; i < touches.length; i++) {
        if (touches[i].identifier == this.activeTouch_)
          return touches[i];
      }
      return undefined;
    },

    /**
     * Touch move handler.
     * @param {!TouchEvent} e The touchmove event.
     * @private
     */
    onMove_: function(e) {
      if (!this.tracking_)
        return;

      // Our active touch should always be in the list of touches still active
      assert(this.findActiveTouch_(e.touches), 'Missing touchEnd');

      var that = this;
      var touch = this.findActiveTouch_(e.changedTouches);
      if (!touch)
        return;

      var clientX = touch.clientX;
      var clientY = touch.clientY;

      var moveX = this.lastTouchX_ - clientX;
      var moveY = this.lastTouchY_ - clientY;
      this.totalMoveX_ += Math.abs(moveX);
      this.totalMoveY_ += Math.abs(moveY);
      this.lastTouchX_ = clientX;
      this.lastTouchY_ = clientY;

      if (!this.dragging_ && (this.totalMoveY_ >
          TouchHandler.MIN_TRACKING_FOR_DRAG_ ||
          this.totalMoveX_ >
          TouchHandler.MIN_TRACKING_FOR_DRAG_)) {
        // If we're waiting for a long press, stop
        window.clearTimeout(this.longPressTimeout_);

        // Dispatch the DRAG_START event and record whether dragging should be
        // allowed or not.  Note that this relies on the current value of
        // startTouchX/Y - handlers may use the initial drag delta to determine
        // if dragging should be permitted.
        this.dragging_ = this.dispatchEvent_(
            TouchHandler.EventType.DRAG_START, touch);

        if (this.dragging_) {
          // Update the start position here so that drag deltas have better
          // values but don't touch the recent positions so that velocity
          // calculations can still use touchstart position in the time and
          // distance delta.
          this.startTouchX_ = clientX;
          this.startTouchY_ = clientY;
          this.startTime_ = e.timeStamp;
        } else {
          this.endTracking_();
        }
      }

      if (this.dragging_) {
        this.dispatchEvent_(TouchHandler.EventType.DRAG_MOVE, touch);

        this.removeTouchesInWrongDirection_(this.recentTouchesX_,
            this.lastMoveX_, moveX);
        this.removeTouchesInWrongDirection_(this.recentTouchesY_,
            this.lastMoveY_, moveY);
        this.removeOldTouches_(this.recentTouchesX_, e.timeStamp);
        this.removeOldTouches_(this.recentTouchesY_, e.timeStamp);
        this.recentTouchesX_.push(clientX, e.timeStamp);
        this.recentTouchesY_.push(clientY, e.timeStamp);
      }

      this.lastMoveX_ = moveX;
      this.lastMoveY_ = moveY;
    },

    /**
     * Filters the provided recent touches array to remove all touches except
     * the last if the move direction has changed.
     * @param {!Array.<number>} recentTouches An array of tuples where the first
     *     item is the x or y component of the recent touch and the second item
     *     is the touch time stamp.
     * @param {number|undefined} lastMove The x or y component of the previous
     *     move.
     * @param {number} recentMove The x or y component of the most recent move.
     * @private
     */
    removeTouchesInWrongDirection_: function(recentTouches, lastMove,
        recentMove) {
      if (lastMove && recentMove && recentTouches.length > 2 &&
          (lastMove > 0 ^ recentMove > 0)) {
        recentTouches.splice(0, recentTouches.length - 2);
      }
    },

    /**
     * Filters the provided recent touches array to remove all touches older
     * than the max tracking time or the 5th most recent touch.
     * @param {!Array.<number>} recentTouches An array of tuples where the first
     *     item is the x or y component of the recent touch and the second item
     *     is the touch time stamp.
     * @param {number} recentTime The time of the most recent event.
     * @private
     */
    removeOldTouches_: function(recentTouches, recentTime) {
      while (recentTouches.length && recentTime - recentTouches[1] >
          TouchHandler.MAX_TRACKING_TIME_ ||
          recentTouches.length >
              TouchHandler.MAX_TRACKING_TOUCHES_ * 2) {
        recentTouches.splice(0, 2);
      }
    },

    /**
     * Touch end handler.
     * @param {!TouchEvent} e The touchend event.
     * @private
     */
    onEnd_: function(e) {
      var that = this;
      assert(this.activeTouch_ !== undefined, 'Expect to already be touching');

      // If the touch we're tracking isn't changing here, ignore this touch end.
      var touch = this.findActiveTouch_(e.changedTouches);
      if (!touch) {
        // In most cases, our active touch will be in the 'touches' collection,
        // but we can't assert that because occasionally two touchend events can
        // occur at almost the same time with both having empty 'touches' lists.
        // I.e., 'touches' seems like it can be a bit more up-to-date than the
        // current event.
        return;
      }

      // This is touchEnd for the touch we're monitoring
      assert(!this.findActiveTouch_(e.touches),
             'Touch ended also still active');

      // Indicate that touching has finished
      this.stopTouching_();

      if (this.tracking_) {
        var clientX = touch.clientX;
        var clientY = touch.clientY;

        if (this.dragging_) {
          this.endTime_ = e.timeStamp;
          this.endTouchX_ = clientX;
          this.endTouchY_ = clientY;

          this.removeOldTouches_(this.recentTouchesX_, e.timeStamp);
          this.removeOldTouches_(this.recentTouchesY_, e.timeStamp);

          this.dispatchEvent_(TouchHandler.EventType.DRAG_END, touch);

          // Note that in some situations we can get a click event here as well.
          // For now this isn't a problem, but we may want to consider having
          // some logic that hides clicks that appear to be caused by a touchEnd
          // used for dragging.
        }

        this.endTracking_();
      }

      // Note that we dispatch the touchEnd event last so that events at
      // different levels of semantics nest nicely (similar to how DOM
      // drag-and-drop events are nested inside of the mouse events that trigger
      // them).
      this.dispatchEvent_(TouchHandler.EventType.TOUCH_END, touch);
    },

    /**
     * Get end velocity of the drag. This method is specific to drag behavior,
     * so if touch behavior and drag behavior is split then this should go with
     * drag behavior. End velocity is defined as deltaXY / deltaTime where
     * deltaXY is the difference between endPosition and the oldest recent
     * position, and deltaTime is the difference between endTime and the oldest
     * recent time stamp.
     * @return {Object} The x and y velocity.
     */
    getEndVelocity: function() {
      // Note that we could move velocity to just be an end-event parameter.
      var velocityX = this.recentTouchesX_.length ?
          (this.endTouchX_ - this.recentTouchesX_[0]) /
          (this.endTime_ - this.recentTouchesX_[1]) : 0;
      var velocityY = this.recentTouchesY_.length ?
          (this.endTouchY_ - this.recentTouchesY_[0]) /
          (this.endTime_ - this.recentTouchesY_[1]) : 0;

      velocityX = this.correctVelocity_(velocityX);
      velocityY = this.correctVelocity_(velocityY);

      return {
        x: velocityX,
        y: velocityY
      };
    },

    /**
     * Correct erroneous velocities by capping the velocity if we think it's too
     * high, or setting it to a default velocity if know that the event data is
     * bad.
     * @param {number} velocity The x or y velocity component.
     * @return {number} The corrected velocity.
     * @private
     */
    correctVelocity_: function(velocity) {
      var absVelocity = Math.abs(velocity);

      // We add to recent touches for each touchstart and touchmove. If we have
      // fewer than 3 touches (6 entries), we assume that the thread was blocked
      // for the duration of the drag and we received events in quick succession
      // with the wrong time stamps.
      if (absVelocity > TouchHandler.MAXIMUM_VELOCITY_) {
        absVelocity = this.recentTouchesY_.length < 3 ?
            TouchHandler.VELOCITY_FOR_INCORRECT_EVENTS_ :
                TouchHandler.MAXIMUM_VELOCITY_;
      }
      return absVelocity * (velocity < 0 ? -1 : 1);
    },

    /**
     * Handler when an element has been pressed for a long time
     * @private
     */
    onLongPress_: function() {
      // Swallow any click that occurs on this element without an intervening
      // touch start event.  This simple click-busting technique should be
      // sufficient here since a real click should have a touchstart first.
      this.swallowNextClick_ = true;

      // Dispatch to the LONG_PRESS
      this.dispatchEventXY_(TouchHandler.EventType.LONG_PRESS, this.element_,
          this.startTouchX_, this.startTouchY_);
    },

    /**
     * Click handler - used to swallow clicks after a long-press
     * @param {!Event} e The click event.
     * @private
     */
    onClick_: function(e) {
      if (this.swallowNextClick_) {
        e.preventDefault();
        e.stopPropagation();
        this.swallowNextClick_ = false;
      }
    },

    /**
     * Dispatch a TouchHandler event to the element
     * @param {string} eventType The event to dispatch.
     * @param {Touch} touch The touch triggering this event.
     * @return {boolean|undefined} The value of enableDrag after dispatching
     *         the event.
     * @private
     */
    dispatchEvent_: function(eventType, touch) {

      // Determine which element was touched.  For mouse events, this is always
      // the event/touch target.  But for touch events, the target is always the
      // target of the touchstart (and it's unlikely we can change this
      // since the common implementation of touch dragging relies on it). Since
      // touch is our primary scenario (which we want to emulate with mouse),
      // we'll treat both cases the same and not depend on the target.
      var touchedElement;
      if (eventType == TouchHandler.EventType.TOUCH_START) {
        touchedElement = touch.target;
      } else {
        touchedElement = this.element_.ownerDocument.
            elementFromPoint(touch.clientX, touch.clientY);
      }

      return this.dispatchEventXY_(eventType, touchedElement, touch.clientX,
          touch.clientY);
    },

    /**
     * Dispatch a TouchHandler event to the element
     * @param {string} eventType The event to dispatch.
       @param {number} clientX The X location for the event.
       @param {number} clientY The Y location for the event.
     * @return {boolean|undefined} The value of enableDrag after dispatching
     *         the event.
     * @private
     */
    dispatchEventXY_: function(eventType, touchedElement, clientX, clientY) {
      var isDrag = (eventType == TouchHandler.EventType.DRAG_START ||
          eventType == TouchHandler.EventType.DRAG_MOVE ||
          eventType == TouchHandler.EventType.DRAG_END);

      // Drag events don't bubble - we're really just dragging the element,
      // not affecting its parent at all.
      var bubbles = !isDrag;

      var event = new TouchHandler.Event(eventType, bubbles, clientX, clientY,
          touchedElement);

      // Set enableDrag when it can be overridden
      if (eventType == TouchHandler.EventType.TOUCH_START)
        event.enableDrag = false;
      else if (eventType == TouchHandler.EventType.DRAG_START)
        event.enableDrag = true;

      if (isDrag) {
        event.dragDeltaX = clientX - this.startTouchX_;
        event.dragDeltaY = clientY - this.startTouchY_;
      }

      this.element_.dispatchEvent(event);
      return event.enableDrag;
    }
  };

  return TouchHandler;
})();
</script>
</head>

<body>
  <div id="notification-container" class="inactive" hidden>
    <div id="notification">
      <span></span>
      <div id="notificationLinks"></div>
      <button class="close-button"></button>
    </div>
  </div>

  <div id="card-slider-frame">
    <button id="page-switcher-start" class="page-switcher" hidden>‹</button>
    <div id="page-list"></div>
    <button id="page-switcher-end" class="page-switcher" hidden>›</button>
    <div id="attribution">
      <span i18n-content="attributionintro"></span>
      <img id="attribution-img">
    </div>
  </div>

  <div id="footer">
    <div id="footer-border"></div>
    <div id="footer-content">
      <img id="logo-img">

      <ul id="dot-list">
      </ul>

      <button id="recently-closed-menu-button">
        <span i18n-content="recentlyclosed"></span>
        <div class="disclosure-triangle"></div>
      </button>

      <div id="trash">
        <span i18n-content="appuninstall"></span>
      </div>
    </div>
  </div>
</body>

<!-- NTP4 intro bubble -->
<div id="ntp4-intro-bubble-contents" hidden>
  <div i18n-content="ntp4_intro_message"></div>
  <a i18n-content="learn_more" i18n-values="href: ntp4_intro_url;"
     target="_blank"></a>
</div>
  
<!-- Apps promo. -->
<div id="apps-promo-extras-template" class="apps-promo-extras" hidden>
  <h3 class="apps-promo-heading"></h3>
  <a class="apps-promo-link g-button-basic"><a/>
</div>

<!-- Bookmark -->
<div id="bookmark-template" class="bookmark" hidden>
  <a class="button bookmark-fills-parent">
    <div class="close-button" hidden></div>
    <span class="button-frame bookmark-fills-parent"></span>
    <span class="favicon bookmark-fills-parent"></span>
  </a>
  <div class="color-stripe"></div>
  <a class="title"></a>
</div>

<!-- Bookmarks Title Wrapper -->
<div id="bookmarks-title-wrapper" class="section-title-wrapper" hidden>
  <div class="section-title-mask"></div>
  <div class="section-title"></div>
</div>

<!-- Bookmarks Top Link -->
<div id="bookmarks-top-link-wrapper" hidden>
  <a i18n-content="bookmarksManagerLinkTitle" class="link-color"></a>
</div>

<!-- Bookmarks Bottom Link -->
<div id="bookmarks-bottom-link-wrapper" hidden>
  <a i18n-content="bookmarksShowAllLinkTitle" class="link-color"></a>
</div>

</html>
<script>var templateData = {"abortedError":"De synchronisatieserver is bezet. Probeer het later opnieuw.","anim":"true","appDefaultPageName":"Applicaties","appcreateshortcut":"Snelkoppeling maken","applaunchtypefullscreen":"Openen in volledig scherm","applaunchtypepinned":"Openen als vastgezet tabblad","applaunchtyperegular":"Openen als normaal tabblad","applaunchtypewindow":"Openen als venster","appoptions":"Opties","apps":"Applicaties","apps_page_id":2048,"appsettings":"Instellingen","appuninstall":"Verwijderen uit Chrome","attributionintro":"Thema gemaakt door","autofill":"Automatisch aanvullen","bookmarkbarattached":"true","bookmarks":"Bladwijzers","bookmarksManagerLinkTitle":"Bladwijzers beheren","bookmarksPage":"Bladwijzers","bookmarksShowAllLinkTitle":"Alles weergeven","bookmarks_page_id":3072,"cancel":"Annuleren","cancelWarning":"Zonder wachtwoordzin kunnen uw wachtwoorden en andere beveiligde gegevens niet op deze computer worden gesynchroniseerd.","cancelWarningHeader":"Synchroniseren van gecodeerde gegevens overslaan?","cannotAccessAccount":"Ik heb geen toegang tot mijn account","cannotAccessAccountURL":"http://www.google.com/support/accounts/bin/answer.py?answer=48598&hl=nl","cannotBeBlank":"Een verplicht veld mag niet leeg zijn","captchaInstructions":"Geef hierboven het juiste wachtwoord op en typ vervolgens de tekens die in de onderstaande afbeelding worden weergegeven.","chooseDataTypes":"Kies wat u wilt synchroniseren","chooseDataTypesInstructions":"Google Chrome synchroniseert uw gegevens veilig met uw Google-account. Houd alles gesynchroniseerd of pas gesynchroniseerde gegevenstypen en opties voor versleuteling aan.","close":"Sluiten","closedwindowmultiple":"$1 tabbladen","closedwindowsingle":"1 tabblad","confirmLabel":"Bevestig de wachtwoordzin","confirmSyncPreferences":"Synchronisatievoorkeuren bevestigen","couldNotConnect":"Kan niet verbinden met server","createAccount":"Een Google-account maken","createNewAccountURL":"https://www.google.com/accounts/NewAccount?service=chromiumsync&hl=nl","customizeLinkLabel":"Geavanceerd","customlogo":"false","downloads":"Downloads","emailLabel":"E-mail:","emptyErrorMessage":"Een lege wachtwoordzin is niet toegestaan.","encryptAllLabel":"Al mijn gegevens coderen","encryptAllOption":"Alle gesynchroniseerde gegevens coderen","encryptSensitiveOption":"Wachtwoorden coderen","encryptedDataTypesTitle":"Ge\u00EBncrypte datatypen","encryptionHelpURL":"http://www.google.com/support/chrome/bin/answer.py?answer=1181035&hl=nl","encryptionInstructions":"U moet in Google Chrome uw gegevens beveiligen met uw Google-wachtwoord of uw eigen wachtwoordzin.","enterAccessCode":"Geef uw toepassingsspecifieke wachtwoord op:","enterGooglePassphraseBody":"U heeft al gegevens beveiligd met een wachtwoordzin. Geef deze hieronder op.","enterOtherPassphraseBody":"De wachtwoordzin die u heeft opgegeven, kan niet worden gebruikt omdat u al gegevens heeft beveiligd met een wachtwoordzin. Geef hieronder uw bestaande wachtwoordzin voor synchronisatie op.","enterPassphraseBody":"U heeft al gegevens beveiligd met een wachtwoordzin. Geef deze hieronder op.","enterPassphraseTitle":"Uw wachtwoordzin voor synchronisatie invoeren","errorSigningIn":"Fout bij aanmelden.","explicitOption":"Mijn eigen wachtwoordzin kiezen","extensions":"Extensies","fontfamily":"Arial, sans-serif","fontsize":"84%","foreignSessions":"Geopende tabbladen","foreignsessions":"Geopende tabbladen","getAccessCodeHelp":"Wat is dit?","getAccessCodeURL":"https://www.google.com/accounts/IssuedAuthSubTokens?hl=nl","googleOption":"Het wachtwoord van mijn Google-account gebruiken","hasattribution":"false","help":"Help","helpurl":"https://www.google.com/support/chrome/?hl=nl","history":"Geschiedenis","incorrectPassphrase":"De ingevoerde wachtwoordzin is onjuist.","introduction":"Met Google Chrome-synchronisatie kunt u uw gegevens (zoals bladwijzers en voorkeuren) makkelijker delen tussen computers.\n        Google Chrome synchroniseert uw gegevens door deze online op te slaan via Google wanneer u zich aanmeldt met uw Google-account.","invalidAccessCode":"Ongeldig toepassingsspecifiek wachtwoord.","invalidCredentials":"De gebruikersnaam of het wachtwoord is ongeldig.","invalidPasswordHelpURL":"http://www.google.com/support/accounts/bin/answer.py?ctx=ch&answer=27444&hl=nl","keepEverythingSynced":"Alles synchroniseren","mismatchErrorMessage":"U moet twee keer dezelfde wachtwoordzin opgeven.","most_visited_page_id":1024,"mostvisited":"Meest bezocht","no":"Wachtwoordzin opgeven","pagedisplaytooltip":"Pagina-indeling wijzigen","passphraseEncryptionMessage":"Uw wachtwoorden worden voor extra beveiliging door Google Chrome gecodeerd.","passphraseLabel":"Wachtwoordzin","passphraseRecover":"Als u uw wachtwoordzin bent vergeten, stopt u Synchroniseren en stelt u de functie opnieuw in via \u003Ca href=\"https://www.google.com/dashboard/?hl=nl\" target=\"_blank\"\u003EGoogle Dashboard\u003C/a\u003E.","passphraseSectionTitle":"Wachtwoordzin voor versleuteling","passphraseWarning":"U gebruikt momenteel een wachtwoordzin. Als u uw wachtwoordzin bent vergeten, kunt u de synchronisatie opnieuw instellen, zodat u uw gegevens van de servers van Google kunt verwijderen met Google Dashboard.","passwordLabel":"Wachtwoord:","passwords":"Wachtwoorden","pinthumbnailtooltip":"Op deze pagina laten staan","preferences":"Voorkeuren","privacyDashboardLink":"Gesynchroniseerde gegevens van Google Dashboard verwijderen","recentlyclosed":"Recent gesloten","removethumbnailtooltip":"Niet op deze pagina weergeven","restoreThumbnailsShort":"Alles herstellen","restorethumbnails":"Alle verwijderde miniaturen herstellen","searchEngines":"Zoekmachines","searchengines":"Zoekmachines","sectionExplicitMessage":"Alleen iemand die uw wachtwoordzin kent, kan uw ge\u00EBncrypte gegevens lezen. De wachtwoordzin wordt niet naar Google verzonden of door Google opgeslagen. Als u uw wachtwoordzin vergeet, moet u de synchronisatie opnieuw instellen.","sectionExplicitMessagePostfix":"synchronisatie opnieuw instellen","sectionExplicitMessagePrefix":"Alleen iemand die uw wachtwoordzin kent, kan uw ge\u00EBncrypte gegevens lezen. De wachtwoordzin wordt niet naar Google verzonden of door Google opgeslagen. Als u uw wachtwoordzin vergeet, moet u","sectionGoogleMessage":"U heeft ervoor gekozen uw gegevens te beveiligen met uw Google-wachtwoord. U kunt dit later desgewenst aanpassen in uw synchronisatie-instellingen.","settingUp":"Bezig met instellen...","showhidelisttooltip":"Lijstweergave","showhidethumbnailtooltip":"Miniatuurweergave","shown_page_index":0,"shown_page_type":1024,"shown_sections":64,"signin":"Aanmelden","signinPrefix":"Aanmelden met uw","signinSuffix":"Google-account","syncEverything":"OK, alles synchroniseren","syncLinkText":"Geavanceerde opties","syncSetupConfigureTitle":"Synchronisatievoorkeuren aanpassen","syncSetupOverlayTitle":"Aanmelden met uw Google-account","syncZeroDataTypesError":"U moet ten minste \u00E9\u00E9n gegevenstype selecteren om te synchroniseren.","syncispresent":"true","syncpromotext":"Synchronisatie instellen...","textdirection":"ltr","themegravity":"","themes":"Thema's","thumbnailremovednotification":"Miniatuur verwijderd.","title":"Nieuw tabblad","typedURLs":"Omniboxgeschiedenis","undothumbnailremove":"Ongedaan maken","unpinthumbnailtooltip":"Niet op deze pagina laten staan","useDefaultSettings":"Standaardinstellingen gebruiken","web_store_title":"Web Store","web_store_url":"https://chrome.google.com/webstore?hl=nl","yes":"Overslaan"};</script><script>// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview This is a simple template engine inspired by JsTemplates
 * optimized for i18n.
 *
 * It currently supports two handlers:
 *
 *   * i18n-content which sets the textContent of the element
 *
 *     <span i18n-content="myContent"></span>
 *     i18nTemplate.process(element, {'myContent': 'Content'});
 *
 *   * i18n-values is a list of attribute-value or property-value pairs.
 *     Properties are prefixed with a '.' and can contain nested properties.
 *
 *     <span i18n-values="title:myTitle;.style.fontSize:fontSize"></span>
 *     i18nTemplate.process(element, {
 *       'myTitle': 'Title',
 *       'fontSize': '13px'
 *     });
 */

var i18nTemplate = (function() {
  /**
   * This provides the handlers for the templating engine. The key is used as
   * the attribute name and the value is the function that gets called for every
   * single node that has this attribute.
   * @type {Object}
   */
  var handlers = {
    /**
     * This handler sets the textContent of the element.
     */
    'i18n-content': function(element, attributeValue, obj) {
      element.textContent = obj[attributeValue];
    },

    /**
     * This handler adds options to a select element.
     */
    'i18n-options': function(element, attributeValue, obj) {
      var options = obj[attributeValue];
      options.forEach(function(values) {
        var option = typeof values == 'string' ? new Option(values) :
            new Option(values[1], values[0]);
        element.appendChild(option);
      });
    },

    /**
     * This is used to set HTML attributes and DOM properties,. The syntax is:
     *   attributename:key;
     *   .domProperty:key;
     *   .nested.dom.property:key
     */
    'i18n-values': function(element, attributeValue, obj) {
      var parts = attributeValue.replace(/\s/g, '').split(/;/);
      for (var j = 0; j < parts.length; j++) {
        var a = parts[j].match(/^([^:]+):(.+)$/);
        if (a) {
          var propName = a[1];
          var propExpr = a[2];

          // Ignore missing properties
          if (propExpr in obj) {
            var value = obj[propExpr];
            if (propName.charAt(0) == '.') {
              var path = propName.slice(1).split('.');
              var object = element;
              while (object && path.length > 1) {
                object = object[path.shift()];
              }
              if (object) {
                object[path] = value;
                // In case we set innerHTML (ignoring others) we need to
                // recursively check the content
                if (path == 'innerHTML') {
                  process(element, obj);
                }
              }
            } else {
              element.setAttribute(propName, value);
            }
          } else {
            console.warn('i18n-values: Missing value for "' + propExpr + '"');
          }
        }
      }
    }
  };

  var attributeNames = [];
  for (var key in handlers) {
    attributeNames.push(key);
  }
  var selector = '[' + attributeNames.join('],[') + ']';

  /**
   * Processes a DOM tree with the {@code obj} map.
   */
  function process(node, obj) {
    var elements = node.querySelectorAll(selector);
    for (var element, i = 0; element = elements[i]; i++) {
      for (var j = 0; j < attributeNames.length; j++) {
        var name = attributeNames[j];
        var att = element.getAttribute(name);
        if (att != null) {
          handlers[name](element, att, obj);
        }
      }
    }
  }

  return {
    process: process
  };
})();
</script><script>// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Invoke the template engine previously loaded from i18n_template.js
i18nTemplate.process(document, templateData);
</script>